@rightkit/release 0.6.10 → 0.6.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -5
- package/admission-files.mjs +3 -0
- package/admission.mjs +2 -2
- package/advisory-exceptions.mjs +41 -5
- package/android-play-upload.mjs +16 -15
- package/app-check.mjs +13 -9
- package/bin/rightkit-gate.mjs +772 -355
- package/broker-pins.mjs +152 -0
- package/build-release.mjs +80 -27
- package/cargo-ledger.mjs +62 -0
- package/cargo-target-shim.mjs +98 -26
- package/cli/right-release.mjs +28 -4
- package/clippy-equivalence.mjs +202 -0
- package/clippy-route.mjs +543 -0
- package/committed-source.mjs +39 -5
- package/crate-workspaces.mjs +87 -0
- package/create-mac-updater.mjs +14 -2
- package/deterministic-scans.mjs +196 -21
- package/development.mjs +13 -4
- package/direct-receipt.mjs +135 -0
- package/door-policy.mjs +10 -0
- package/door.mjs +6 -6
- package/freshness-census.mjs +257 -0
- package/gate-stages.mjs +76 -10
- package/hook-retirement.mjs +65 -0
- package/init-command.mjs +1 -1
- package/inventory-check.mjs +11 -8
- package/ios-upload.mjs +20 -15
- package/local-publish.mjs +77 -41
- package/local-qualify.mjs +5 -3
- package/local-runtime.mjs +5 -4
- package/mac-package-recipe.mjs +4 -1
- package/mirror-root-artifact.mjs +18 -5
- package/ownership-class-rules.mjs +1 -1
- package/ownership-evidence.mjs +37 -13
- package/ownership-hooks.mjs +20 -59
- package/ownership-lockfiles.mjs +14 -1
- package/ownership-policy-bundle.mjs +3 -2
- package/ownership-staged.mjs +2 -1
- package/ownership.mjs +24 -0
- package/package-recipe-targets.mjs +35 -6
- package/package.json +1 -1
- package/pipeline-admission.mjs +4 -4
- package/preflight.mjs +27 -4
- package/publish-cargo.mjs +4 -3
- package/recipe-build.mjs +38 -14
- package/release-artifact-path.mjs +341 -0
- package/release-invocation.mjs +8 -1
- package/release-qa-command.mjs +25 -9
- package/release-source-brand.mjs +17 -0
- package/release-source-identity.mjs +171 -0
- package/release.mjs +44 -17
- package/rightkit-versions.json +63 -42
- package/runtime-budgets.mjs +6 -2
- package/scan-digest.mjs +71 -5
- package/sign-updater.mjs +6 -3
- package/snapshot-broker.mjs +172 -0
- package/snapshot-check.mjs +12 -6
- package/snapshot-request.mjs +207 -5
- package/snapshot.mjs +123 -10
- package/source-split.mjs +73 -0
- package/stage-commands.mjs +2 -2
- package/stage-ledger.mjs +204 -0
- package/stage-sidecar-command.mjs +5 -1
- package/stage-sidecar.mjs +100 -20
- package/swap-helper-bundle.mjs +5 -2
- package/unit-gate.mjs +45 -3
- package/web-build.mjs +6 -4
- package/win-package-recipe.mjs +20 -7
package/ownership-hooks.mjs
CHANGED
|
@@ -1,41 +1,22 @@
|
|
|
1
|
-
// right-release hooks
|
|
1
|
+
// right-release hooks: the advisory pre-commit ownership gate is retired (docs/GATE-CONTRACT.md: "Commit and push run no gate and no
|
|
2
|
+
// local hook checks"). Nothing here writes a hook any more.
|
|
2
3
|
//
|
|
3
|
-
// right-release hooks install [--app <dir>]
|
|
4
|
+
// right-release hooks install [--app <dir>] (`right-release hooks install` is `rightkit-gate install-hook`: it retires the hook)
|
|
4
5
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
import {
|
|
6
|
+
// An earlier release wrote `.git/hooks/pre-commit` (or the one in core.hooksPath) running `right-release ownership --staged`, and
|
|
7
|
+
// kept a foreign pre-commit as pre-commit.rightkit-foreign. Retirement removes the hook by its own marker only and puts the foreign
|
|
8
|
+
// pre-commit back. `right-release ownership --staged` stays a manual command; broker admission and the release gate remain
|
|
9
|
+
// authoritative.
|
|
10
|
+
import { lstatSync, readFileSync, readdirSync, realpathSync, renameSync, rmdirSync, unlinkSync } from "node:fs";
|
|
10
11
|
import path from "node:path";
|
|
11
12
|
import { fileURLToPath } from "node:url";
|
|
12
13
|
import { spawnSync } from "./background-process.mjs";
|
|
14
|
+
import { HOOK_MARKER, retireHooksIn } from "./hook-retirement.mjs";
|
|
13
15
|
import { repositoryRootFor } from "./ownership-staged.mjs";
|
|
14
16
|
|
|
15
|
-
export
|
|
16
|
-
const FOREIGN = "pre-commit.rightkit-foreign";
|
|
17
|
-
const PINNED_CLI = path.join("node_modules", "@rightkit", "release", "cli", "right-release.mjs");
|
|
17
|
+
export { HOOK_MARKER };
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
return [
|
|
21
|
-
"#!/bin/sh",
|
|
22
|
-
`# ${HOOK_MARKER}: advisory ownership gate installed by \`right-release hooks install\`.`,
|
|
23
|
-
"# Block-tier findings without a valid exception fail here; --no-verify bypasses this gate.",
|
|
24
|
-
"# Authoritative enforcement remains in broker admission and the release gate.",
|
|
25
|
-
"ROOT=\"$(git rev-parse --show-toplevel)\" || exit 1",
|
|
26
|
-
"HOOKS_DIR=\"$(dirname \"$0\")\"",
|
|
27
|
-
`if [ -x "$HOOKS_DIR/${FOREIGN}" ]; then`,
|
|
28
|
-
` "$HOOKS_DIR/${FOREIGN}" "$@" || exit $?`,
|
|
29
|
-
"fi",
|
|
30
|
-
`GATE="$ROOT/${PINNED_CLI.split(path.sep).join("/")}"`,
|
|
31
|
-
"if [ ! -f \"$GATE\" ]; then",
|
|
32
|
-
" echo \"[rightkit] @rightkit/release is not installed in this repository; add it as a devDependency, then re-run: right-release hooks install\" >&2",
|
|
33
|
-
" exit 1",
|
|
34
|
-
"fi",
|
|
35
|
-
"exec node \"$GATE\" ownership --staged",
|
|
36
|
-
"",
|
|
37
|
-
].join("\n");
|
|
38
|
-
}
|
|
19
|
+
const FS_OPS = Object.freeze({ readdir: readdirSync, readFile: (file) => readFileSync(file, "utf8"), lstat: lstatSync, unlink: unlinkSync, rename: renameSync, rmdir: rmdirSync });
|
|
39
20
|
|
|
40
21
|
function git(cwd, args) {
|
|
41
22
|
const result = spawnSync("git", ["-C", cwd, ...args], { encoding: "utf8", windowsHide: true, env: Object.fromEntries(Object.entries(process.env).filter(([k]) => !/^GIT_/i.test(k))) });
|
|
@@ -52,46 +33,26 @@ export function hooksDirectoryFor(root) {
|
|
|
52
33
|
return path.resolve(root, common);
|
|
53
34
|
}
|
|
54
35
|
|
|
55
|
-
/**
|
|
56
|
-
|
|
57
|
-
*/
|
|
58
|
-
export function installPreCommitHook(repoDir) {
|
|
36
|
+
/** Remove the ownership pre-commit hook an earlier release wrote, restoring the foreign pre-commit it displaced. { status: "retired" | "restored" | "absent", file } */
|
|
37
|
+
export function retirePreCommitHook(repoDir) {
|
|
59
38
|
const root = repositoryRootFor(repoDir);
|
|
60
|
-
const pinned = path.join(root, PINNED_CLI);
|
|
61
|
-
if (!existsSync(pinned)) throw new Error(`@rightkit/release is not installed at ${pinned}; add it as a devDependency and run pnpm install first`);
|
|
62
39
|
const hooksDir = hooksDirectoryFor(root);
|
|
40
|
+
const result = retireHooksIn(hooksDir, FS_OPS, { scripts: false });
|
|
63
41
|
const file = path.join(hooksDir, "pre-commit");
|
|
64
|
-
|
|
65
|
-
const body = renderPreCommitHook();
|
|
66
|
-
mkdirSync(hooksDir, { recursive: true });
|
|
67
|
-
let status = "installed";
|
|
68
|
-
if (lstatSync(file, { throwIfNoEntry: false })) {
|
|
69
|
-
const current = readFileSync(file, "utf8");
|
|
70
|
-
if (current === body) return { status: "unchanged", file };
|
|
71
|
-
if (current.includes(HOOK_MARKER)) status = "updated";
|
|
72
|
-
else {
|
|
73
|
-
if (existsSync(path.join(hooksDir, FOREIGN))) throw new Error(`${FOREIGN} already exists; resolve the chained hook manually before installing`);
|
|
74
|
-
renameSync(file, foreign);
|
|
75
|
-
status = "chained";
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
const temporary = `${file}.${process.pid}.tmp`;
|
|
79
|
-
writeFileSync(temporary, body, { mode: 0o755 });
|
|
80
|
-
chmodSync(temporary, 0o755);
|
|
81
|
-
renameSync(temporary, file);
|
|
82
|
-
return { status, file };
|
|
42
|
+
return { status: result.restored.length ? "restored" : result.removed.includes("pre-commit") ? "retired" : "absent", file };
|
|
83
43
|
}
|
|
84
44
|
|
|
85
45
|
export function main(args, { print = (line) => console.log(line) } = {}) {
|
|
86
46
|
const flag = (name) => { const i = args.indexOf(name); return i >= 0 ? args[i + 1] : undefined; };
|
|
87
47
|
if (args[0] !== "install") {
|
|
88
|
-
print("right-release hooks install [--app <dir>]");
|
|
48
|
+
print("right-release hooks install [--app <dir>] (retires the ownership pre-commit hook; no hook is written)");
|
|
89
49
|
return args.includes("--help") ? 0 : 2;
|
|
90
50
|
}
|
|
91
51
|
try {
|
|
92
|
-
const result =
|
|
93
|
-
print(`[right-release] pre-commit ownership gate ${result.status}: ${result.file}`);
|
|
94
|
-
if (result.status === "
|
|
52
|
+
const result = retirePreCommitHook(path.resolve(flag("--app") ?? process.cwd()));
|
|
53
|
+
print(`[right-release] pre-commit ownership gate retired (${result.status}): ${result.file}`);
|
|
54
|
+
if (result.status === "restored") print("[right-release] the previous pre-commit hook is back in place");
|
|
55
|
+
print("[right-release] commit runs no local hook check; `right-release ownership --staged` is still available by hand");
|
|
95
56
|
return 0;
|
|
96
57
|
} catch (error) {
|
|
97
58
|
print(`[right-release] hooks install FAILED: ${error.message}`);
|
package/ownership-lockfiles.mjs
CHANGED
|
@@ -322,9 +322,22 @@ export function transitiveDependencies(appRoot, inventory, manifests, catalogOwn
|
|
|
322
322
|
if (bytes && !contentDigests.some((item) => item.path === rel)) contentDigests.push({ path: rel, digest: sha256(bytes) });
|
|
323
323
|
return bytes;
|
|
324
324
|
};
|
|
325
|
+
// Directories the repository declares as gate fixtures (ci/gate.config.json `fixtures`: consumer recipes, vendored crates,
|
|
326
|
+
// spikes) are samples and harnesses, never the app's code: the gate builds none of them as units, and neither are they scanned here.
|
|
327
|
+
let fixtures = [];
|
|
328
|
+
try {
|
|
329
|
+
const rel = "ci/gate.config.json";
|
|
330
|
+
const config = inventory.snapshotFiles?.get(`${inventory.pathPrefix ?? ""}${rel}`)?.bytes ?? files.get(rel)?.bytes
|
|
331
|
+
?? (!inventory.committed && existsSync(path.resolve(appRoot, rel)) ? readFileSync(path.resolve(appRoot, rel)) : undefined);
|
|
332
|
+
if (config) fixtures = (JSON.parse(config.toString("utf8")).fixtures ?? []).map((d) => String(d).replace(/\/+$/, ""));
|
|
333
|
+
} catch { fixtures = []; }
|
|
334
|
+
const underFixture = (rel) => fixtures.some((d) => rel === d || rel.startsWith(`${d}/`));
|
|
325
335
|
for (const manifest of manifests) {
|
|
326
336
|
const base = path.posix.basename(manifest.rel), ecosystem = base === "Cargo.toml" ? "cargo" : base === "package.json" ? "npm" : null;
|
|
327
337
|
if (!ecosystem || !manifest.bytes) continue;
|
|
338
|
+
if (underFixture(manifest.rel)) continue;
|
|
339
|
+
// A Cargo manifest with its own [workspace] table is a workspace root: its lock sits beside it and Cargo never looks higher.
|
|
340
|
+
const workspaceRoot = ecosystem === "cargo" && /^\s*\[workspace\]\s*$/m.test(manifest.bytes.toString("utf8"));
|
|
328
341
|
const names = ecosystem === "cargo" ? ["Cargo.lock"] : OWNERSHIP_LOCKFILES.slice(1);
|
|
329
342
|
let dir = path.posix.dirname(manifest.rel), found = false;
|
|
330
343
|
try {
|
|
@@ -337,7 +350,7 @@ export function transitiveDependencies(appRoot, inventory, manifests, catalogOwn
|
|
|
337
350
|
}
|
|
338
351
|
found = true; break;
|
|
339
352
|
}
|
|
340
|
-
if (dir === ".") break;
|
|
353
|
+
if (dir === "." || workspaceRoot) break;
|
|
341
354
|
dir = path.posix.dirname(dir);
|
|
342
355
|
}
|
|
343
356
|
if (!found) diagnostic(manifest.rel, "lockfile-missing", ecosystem);
|
|
@@ -17,10 +17,11 @@ const CATALOG_FILE = "rightkit-ownership.json";
|
|
|
17
17
|
const REGISTRY_FILE = "ownership-repositories.json";
|
|
18
18
|
// Step 4: the release QA requirement floor is protected like the catalog. Dropping a required check changes the digest.
|
|
19
19
|
export const QA_FLOOR_FILE = "release-qa-floor.json";
|
|
20
|
-
// Modules that make or carry enforcement decisions. Their import closure is pinned.
|
|
20
|
+
// Modules that make or carry enforcement decisions. Their import closure is pinned. ownership-hooks.mjs left the list with the
|
|
21
|
+
// advisory pre-commit hook it used to write (GATE-CONTRACT: commit runs no local hook check): it retires that hook and decides nothing.
|
|
21
22
|
export const POLICY_BUNDLE_ENTRYPOINTS = Object.freeze([
|
|
22
23
|
"ownership-scan-worker.mjs", "ownership-evidence.mjs", "ownership-verdict.mjs", "ownership-admission.mjs",
|
|
23
|
-
"ownership-staged.mjs", "ownership-
|
|
24
|
+
"ownership-staged.mjs", "ownership-key-trust.mjs", "release-qa-receipt.mjs",
|
|
24
25
|
]);
|
|
25
26
|
// Data files are pinned through their own digests, not as source files.
|
|
26
27
|
const DATA_FILES = new Set([CATALOG_FILE, REGISTRY_FILE, ALLOWLIST_FILE, QA_FLOOR_FILE]);
|
package/ownership-staged.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Ownership gate for the STAGED tree (index content), never the working tree: the manual command `right-release ownership --staged`.
|
|
2
|
+
// No git hook runs it any more (docs/GATE-CONTRACT.md: commit runs no local hook check).
|
|
2
3
|
// Authoritative enforcement stays in broker admission and the release gate.
|
|
3
4
|
//
|
|
4
5
|
// right-release ownership --staged [--app <dir>] [--json]
|
package/ownership.mjs
CHANGED
|
@@ -382,6 +382,9 @@ function resolveDependencyVersions(appRoot, inventory, deps, coverage, contentDi
|
|
|
382
382
|
const refs = lock.package.filter((p) => p.name === owner).flatMap((p) => p.dependencies ?? []);
|
|
383
383
|
candidates = candidates.filter((p) => refs.some((ref) => ref === `${d.name} ${p.version}` || ref.startsWith(`${d.name} ${p.version} (`)));
|
|
384
384
|
}
|
|
385
|
+
// A crate may declare two versions of one package under different names (windows 0.61 and 0.62 lanes): the declared
|
|
386
|
+
// requirement picks the resolved one, by Cargo's default caret rule (or an exact `=` pin).
|
|
387
|
+
if (candidates.length > 1 && typeof d.requirement === "string") candidates = candidates.filter((p) => cargoRequirementMatches(d.requirement, p.version));
|
|
385
388
|
if (candidates.length !== 1 || typeof candidates[0].version !== "string") throw new Error(`Cargo.lock resolution missing or ambiguous: ${d.name}`);
|
|
386
389
|
d.resolvedVersion = candidates[0].version;
|
|
387
390
|
}
|
|
@@ -917,3 +920,24 @@ export function checkCommittedApp(appRoot, manifest, options = {}) {
|
|
|
917
920
|
export function checkCommittedAppForTest(appRoot, manifest, options = {}) {
|
|
918
921
|
return checkCommitted(appRoot, manifest, options, options.acquireCommittedSourceForTest ?? acquireCommittedSource, options.readAuthorityForTest);
|
|
919
922
|
}
|
|
923
|
+
|
|
924
|
+
/** Cargo's default requirement (`0.61`, `^1.2`, `=0.3.4`, `>=2.10.1, <3`) against a resolved version; anything else matches. */
|
|
925
|
+
function cargoRequirementMatches(requirement, version) {
|
|
926
|
+
const parse = (v) => String(v).split("+")[0].split("-")[0].split(".").map((n) => Number(n));
|
|
927
|
+
const [x, y = 0, z = 0] = parse(version);
|
|
928
|
+
const cmp = (a, b) => a[0] - b[0] || (a[1] ?? 0) - (b[1] ?? 0) || (a[2] ?? 0) - (b[2] ?? 0);
|
|
929
|
+
return requirement.split(",").map((r) => r.trim()).filter(Boolean).every((r) => {
|
|
930
|
+
const m = /^(=|\^|>=|<=|>|<|~)?\s*(\d+)(?:\.(\d+))?(?:\.(\d+))?/.exec(r);
|
|
931
|
+
if (!m) return true;
|
|
932
|
+
const op = m[1] ?? "^"; const want = [Number(m[2]), m[3] === undefined ? undefined : Number(m[3]), m[4] === undefined ? undefined : Number(m[4])];
|
|
933
|
+
const v = [x, y, z]; const c = cmp(v, want);
|
|
934
|
+
if (op === "=") return want.every((n, i) => n === undefined || n === v[i]);
|
|
935
|
+
if (op === ">=") return c >= 0; if (op === ">") return c > 0; if (op === "<=") return c <= 0; if (op === "<") return c < 0;
|
|
936
|
+
if (op === "~") return c >= 0 && v[0] === want[0] && (want[1] === undefined || v[1] === want[1]);
|
|
937
|
+
if (c < 0) return false;
|
|
938
|
+
if (want[0] > 0) return v[0] === want[0];
|
|
939
|
+
if (want[1] === undefined) return v[0] === 0;
|
|
940
|
+
if (want[1] > 0) return v[0] === 0 && v[1] === want[1];
|
|
941
|
+
return v[0] === 0 && v[1] === 0 && (want[2] === undefined || v[2] === want[2]);
|
|
942
|
+
});
|
|
943
|
+
}
|
|
@@ -8,6 +8,7 @@ import { existsSync } from "node:fs";
|
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
import { resolveTargetRoot as cargoResolveTargetRoot } from "./cargo-target.mjs";
|
|
11
|
+
import { ArtifactPathError, SPLIT_TARGET_UNRESOLVED_CODE, isSplitArtifactEnv } from "./release-artifact-path.mjs";
|
|
11
12
|
import { defaultManagedRoots, isUnderManagedRoot } from "./managed-target-stage.mjs";
|
|
12
13
|
import { NativeHelpersError, nativeHelperNestedCode, validateNativeHelpers } from "./native-helpers.mjs";
|
|
13
14
|
import { validateWinPackage, WinPackageConfigError } from "./win-package-recipe.mjs";
|
|
@@ -79,13 +80,13 @@ const configArgs = (extras, appRoot, cwd) => (extras.tauriConfig ? ["--config",
|
|
|
79
80
|
* release.mjs runs ONE prePackage command: node recipe-build.mjs runs the built-in cleanup, prepare, the Tauri build,
|
|
80
81
|
* the artifact stability poll and postBuild in order. `cleanup` and `stability` are recipe-build plan sections.
|
|
81
82
|
*/
|
|
82
|
-
function recipeBuildCommand({ extras, build, env, cleanup, stability, stage, timeoutMs, verifySidecars, nativeHelpers }) {
|
|
83
|
+
function recipeBuildCommand({ extras, build, env, cleanup, stability, stage, timeoutMs, verifySidecars, nativeHelpers, boundTargets }) {
|
|
83
84
|
const steps = [
|
|
84
85
|
...extras.prepare.map((step) => ({ phase: "prepare", ...step })),
|
|
85
86
|
{ phase: "build", cmd: build.cmd, args: build.args, cwd: build.cwd },
|
|
86
87
|
...extras.postBuild.map((step) => ({ phase: "postBuild", ...step })),
|
|
87
88
|
];
|
|
88
|
-
const plan = { version: 1, steps, ...(cleanup ? { cleanup } : {}), ...(stability ? { stability } : {}), ...(stage && (stage.pre?.length || stage.post?.length) ? { stage } : {}), ...(verifySidecars ? { verifySidecars } : {}), ...(nativeHelpers ? { nativeHelpers } : {}) };
|
|
89
|
+
const plan = { version: 1, steps, ...(boundTargets?.length ? { boundTargets } : {}), ...(cleanup ? { cleanup } : {}), ...(stability ? { stability } : {}), ...(stage && (stage.pre?.length || stage.post?.length) ? { stage } : {}), ...(verifySidecars ? { verifySidecars } : {}), ...(nativeHelpers ? { nativeHelpers } : {}) };
|
|
89
90
|
return { cmd: process.execPath, args: [RECIPE_BUILD, "--plan", JSON.stringify(plan)], env, ...(timeoutMs ? { timeoutMs } : {}) };
|
|
90
91
|
}
|
|
91
92
|
|
|
@@ -114,6 +115,9 @@ function targetRootMapper({ spec, appRoot, cwd, env, resolveTargetRoot, label, m
|
|
|
114
115
|
function buildTargetRootMapper({ spec, appRoot, cwd, env, resolveTargetRoot, label, managedRoots, staged }) {
|
|
115
116
|
const explicit = spec.cargoTargetRoot;
|
|
116
117
|
if (explicit !== undefined && explicit !== true && explicit !== false && typeof explicit !== "string") fail(`${label}.cargoTargetRoot must be true or an app-relative Cargo.toml path`);
|
|
118
|
+
// A door split (row 7.04c, R-T1/R-T3): the snapshot has no target directory, so every declared target path is the bound target's, absolute, and
|
|
119
|
+
// nothing is copied (the #924 bridge plans zero staged entries): the build output is read and signed in place.
|
|
120
|
+
if (isSplitArtifactEnv(env)) return splitTargetRootMapper({ spec, appRoot, cwd, resolveTargetRoot, label });
|
|
117
121
|
const envDir = typeof env.CARGO_TARGET_DIR === "string" && env.CARGO_TARGET_DIR.trim() !== "";
|
|
118
122
|
if (explicit === false || (!explicit && !envDir)) return (declared) => declared;
|
|
119
123
|
const manifestRel = typeof explicit === "string" ? relativeInside(explicit, `${label}.cargoTargetRoot`) : path.posix.join(cwd === "." ? "" : cwd, "src-tauri", "Cargo.toml");
|
|
@@ -141,9 +145,34 @@ function buildTargetRootMapper({ spec, appRoot, cwd, env, resolveTargetRoot, lab
|
|
|
141
145
|
};
|
|
142
146
|
}
|
|
143
147
|
|
|
144
|
-
function
|
|
148
|
+
function splitTargetRootMapper({ spec, appRoot, cwd, resolveTargetRoot, label }) {
|
|
149
|
+
const explicit = spec.cargoTargetRoot;
|
|
150
|
+
const manifestRel = typeof explicit === "string" ? relativeInside(explicit, `${label}.cargoTargetRoot`) : path.posix.join(cwd === "." ? "" : cwd, "src-tauri", "Cargo.toml");
|
|
151
|
+
const manifest = path.resolve(appRoot, manifestRel);
|
|
152
|
+
// An app that declares no recipe target and has no src-tauri manifest has nothing to bind; a declared one that is missing is refused below.
|
|
153
|
+
if (explicit !== true && typeof explicit !== "string" && !existsSync(manifest)) return (declared) => declared;
|
|
154
|
+
let targetDir;
|
|
155
|
+
try { targetDir = resolveTargetRoot(manifest); } catch (error) {
|
|
156
|
+
throw new ArtifactPathError(SPLIT_TARGET_UNRESOLVED_CODE, `${label}: cannot resolve the Cargo target directory of ${manifest} (${error.message}); a split never falls back to a snapshot-relative target/`);
|
|
157
|
+
}
|
|
158
|
+
const relativeToApp = path.relative(appRoot, targetDir);
|
|
159
|
+
if (relativeToApp === "" || (!relativeToApp.startsWith("..") && !path.isAbsolute(relativeToApp))) {
|
|
160
|
+
throw new ArtifactPathError(SPLIT_TARGET_UNRESOLVED_CODE, `${label}: the Cargo target directory ${targetDir} is inside the snapshot; a split binds it outside (CARGO_TARGET_DIR, row 7.09)`);
|
|
161
|
+
}
|
|
162
|
+
const prefix = path.posix.join(path.posix.dirname(manifestRel).replace(/^\.$/, ""), "target");
|
|
163
|
+
const mapper = (declared) => {
|
|
164
|
+
const normalized = String(declared).replaceAll("\\", "/");
|
|
165
|
+
if (normalized !== prefix && !normalized.startsWith(`${prefix}/`)) return declared;
|
|
166
|
+
if (normalized.split("/").includes("..")) fail(`${label}: ${declared} must not contain ..`);
|
|
167
|
+
return path.resolve(targetDir, normalized.slice(prefix.length + 1));
|
|
168
|
+
};
|
|
169
|
+
mapper.bound = [targetDir];
|
|
170
|
+
return mapper;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function winPrePackage({ extras, cwd, appRoot, triple, jobs, timeoutMs, cleanup, stability, stage, boundTargets }) {
|
|
145
174
|
const args = ["exec", "tauri", "build", "--no-bundle", "--target", triple, ...configArgs(extras, appRoot, cwd), ...buildCargoArgs(extras, jobs)];
|
|
146
|
-
return recipeBuildCommand({ extras, build: { cmd: "pnpm", args, cwd }, env: QUIET_TAURI_ENV, cleanup, stability, stage, timeoutMs });
|
|
175
|
+
return recipeBuildCommand({ extras, build: { cmd: "pnpm", args, cwd }, env: QUIET_TAURI_ENV, cleanup, stability, stage, timeoutMs, boundTargets });
|
|
147
176
|
}
|
|
148
177
|
|
|
149
178
|
/** `targets.win.winPackage` → prePackage (`tauri build --no-bundle`) and package (`tauri bundle --bundles nsis`). */
|
|
@@ -193,7 +222,7 @@ export function expandWinPackageTarget(target, { appRoot, config = {}, env = pro
|
|
|
193
222
|
return {
|
|
194
223
|
...rest,
|
|
195
224
|
signingContract: "windows-raw-exe-authenticode-before-nsis-v1",
|
|
196
|
-
prePackage: winPrePackage({ extras, cwd, appRoot: plan.appRoot, triple: plan.triple, jobs, timeoutMs: timeout, cleanup, stability: { files: [rawExe] }, stage: prePackageStage }),
|
|
225
|
+
prePackage: winPrePackage({ extras, cwd, appRoot: plan.appRoot, triple: plan.triple, jobs, timeoutMs: timeout, cleanup, stability: { files: [rawExe] }, stage: prePackageStage, boundTargets: mapPath.bound }),
|
|
197
226
|
package: bundleCommand,
|
|
198
227
|
sign: { ...(target.sign ?? {}), prePackageFiles: [rawExe], files: [installerFile] },
|
|
199
228
|
recipe: { kind: "winPackage", productName: plan.productName, updater: plan.updater },
|
|
@@ -327,7 +356,7 @@ export function expandMacPackageTarget(target, { appRoot, config = {}, env = pro
|
|
|
327
356
|
return {
|
|
328
357
|
...rest,
|
|
329
358
|
...postSign,
|
|
330
|
-
prePackage: wrapped ? recipeBuildCommand({ extras, build: { cmd: "pnpm", args, cwd }, env: QUIET_TAURI_ENV, cleanup, stage: macStage, verifySidecars, nativeHelpers: nativeHelpersPlan }) : { cmd: "pnpm", args, cwd, env: QUIET_TAURI_ENV },
|
|
359
|
+
prePackage: wrapped ? recipeBuildCommand({ extras, build: { cmd: "pnpm", args, cwd }, env: QUIET_TAURI_ENV, cleanup, stage: macStage, verifySidecars, nativeHelpers: nativeHelpersPlan, boundTargets: mapPath.bound }) : { cmd: "pnpm", args, cwd, env: QUIET_TAURI_ENV },
|
|
331
360
|
// Helpers are signed by release.mjs inside-out through sign.nestedCode (deepest path first, hardened runtime, each helper's own entitlements), then the app.
|
|
332
361
|
sign: { ...(target.sign ?? {}), ...(helpers ? { nestedCode: { ...nativeHelperNestedCode(helpers), ...(target.sign?.nestedCode ?? {}) } } : {}), prePackageFiles },
|
|
333
362
|
notarize: { ...target.notarize, app, appBeforePackage: true },
|
package/package.json
CHANGED
package/pipeline-admission.mjs
CHANGED
|
@@ -239,13 +239,13 @@ function admitRuntime(bin, rest, options, label) {
|
|
|
239
239
|
}
|
|
240
240
|
|
|
241
241
|
/**
|
|
242
|
-
* `rightkit-gate install-hook`:
|
|
243
|
-
* It builds, signs, packages and uploads nothing. The gate itself (`rightkit-gate`, `--base`) compiles and is run by
|
|
244
|
-
*
|
|
242
|
+
* `rightkit-gate install-hook`: retires the git hooks an earlier release wrote and registers the agent hook, so an app's `prepare` script may run it.
|
|
243
|
+
* It builds, signs, packages and uploads nothing. The gate itself (`rightkit-gate`, `--base`) compiles and is never run by a release
|
|
244
|
+
* config; only the installer and the version probe are admitted.
|
|
245
245
|
*/
|
|
246
246
|
function admitRightKitGate(args, label) {
|
|
247
247
|
if (args.length === 1 && (args[0] === "install-hook" || args[0] === "--version")) return admit(`rightkit-gate-${args[0].replace(/^--/, "")}`, "owner");
|
|
248
|
-
return refuse("R1-owner-subcommand", `${label}: only rightkit-gate install-hook (and --version) is an admitted pipeline command; the gate itself
|
|
248
|
+
return refuse("R1-owner-subcommand", `${label}: only rightkit-gate install-hook (and --version) is an admitted pipeline command; the gate itself is run through the door, never by a release config`);
|
|
249
249
|
}
|
|
250
250
|
|
|
251
251
|
function admitRightRelease(args, label) {
|
package/preflight.mjs
CHANGED
|
@@ -23,6 +23,7 @@ import path from "node:path";
|
|
|
23
23
|
import { spawnSync } from "./background-process.mjs";
|
|
24
24
|
import { brokerManagedHost } from "./cargo-contract.mjs";
|
|
25
25
|
import { createOwnershipEvidence } from "./ownership-evidence.mjs";
|
|
26
|
+
import { isReleaseSource } from "./release-source-brand.mjs";
|
|
26
27
|
import { appRuntimeBudgetAdoptionCheck } from "./runtime-budgets.mjs";
|
|
27
28
|
import { isSwiftProject } from "./project-type.mjs";
|
|
28
29
|
|
|
@@ -203,6 +204,12 @@ export function collectPreflight({
|
|
|
203
204
|
bridge = true,
|
|
204
205
|
msix = false,
|
|
205
206
|
findSdkTool = findWindowsSdkTool,
|
|
207
|
+
// A door source split (release-source-identity.mjs `resolveReleaseSource`, row 7.04). Only the ownership check reads it: the evidence is
|
|
208
|
+
// then the selected commit's, read from the checkout's object database, whatever `repoRoot` the caller names.
|
|
209
|
+
releaseSource = null,
|
|
210
|
+
// The artifact-path context of a door split (release-artifact-path.mjs, row 7.04c). When it is a split, the target-bridge check is replaced by
|
|
211
|
+
// the R-T1 check: every manifest has a bound target outside the snapshot, and nothing sits where a snapshot-relative target/ would.
|
|
212
|
+
splitTargets = null,
|
|
206
213
|
} = {}) {
|
|
207
214
|
const checks = [];
|
|
208
215
|
const windows = platform === "win" || platform === "win32";
|
|
@@ -219,7 +226,14 @@ export function collectPreflight({
|
|
|
219
226
|
);
|
|
220
227
|
}
|
|
221
228
|
|
|
222
|
-
if (appRoot && !noBridge) {
|
|
229
|
+
if (appRoot && !noBridge && splitTargets?.mode === "split") {
|
|
230
|
+
const planted = splitTargets.boundTargets.map((bound) => path.join(appRoot, bound.prefix)).filter((dir) => { try { lstatSync(dir); return true; } catch { return false; } });
|
|
231
|
+
checks.push(
|
|
232
|
+
planted.length
|
|
233
|
+
? fail("split-target", `RK_SPLIT_TARGET_PATH: ${planted.join(", ")} exists in the snapshot; a split binds its target outside it and nothing may sit there`, "remove it from the commit (it is build output) and add the directory to .gitignore")
|
|
234
|
+
: ok("split-target", splitTargets.boundTargets.length ? `target bound outside the snapshot: ${[...new Set(splitTargets.boundTargets.map((bound) => bound.dir))].join(", ")}` : "no target to bind"),
|
|
235
|
+
);
|
|
236
|
+
} else if (appRoot && !noBridge) {
|
|
223
237
|
const target = targetLink ?? nativeLayout?.targetLink ?? path.join(appRoot, "src-tauri", "target");
|
|
224
238
|
const targetLabel = nativeLayout?.targetPrefix || "src-tauri/target/";
|
|
225
239
|
let entry = null;
|
|
@@ -339,8 +353,12 @@ export function collectPreflight({
|
|
|
339
353
|
checks.push(fail("ownership", `ownership scope refused: ${ownershipScopeError}`, "fix the target's ownership scope configuration; the build refuses it too"));
|
|
340
354
|
} else if (repoRoot && ownership) {
|
|
341
355
|
// An app living inside the RightKit workspace repository is scoped to its own directory.
|
|
342
|
-
|
|
343
|
-
|
|
356
|
+
// In a split the layout is the snapshot's (the commit's), the git root is the checkout, and the source is the one the identity module resolved.
|
|
357
|
+
if (releaseSource !== null && !isReleaseSource(releaseSource)) throw new Error("preflight: the release source was not resolved by resolveReleaseSource");
|
|
358
|
+
const split = releaseSource?.mode === "snapshot" ? releaseSource : null;
|
|
359
|
+
const gitRoot = split ? split.checkout : repoRoot;
|
|
360
|
+
const workspace = existsSync(path.join(split ? split.snapshot : repoRoot, "tools", "rightkit", "packages", "release"));
|
|
361
|
+
checks.push(ownershipCheck(gitRoot, { scanRoot: workspace && appRoot ? appRoot : gitRoot, scope: ownershipScope, evidenceFactory: ownershipEvidenceFactory, ...(split ? { releaseSource: split } : {}) }));
|
|
344
362
|
}
|
|
345
363
|
|
|
346
364
|
return checks;
|
|
@@ -349,9 +367,14 @@ export function collectPreflight({
|
|
|
349
367
|
/** Release-blocking: the app keeps no unexcepted copy of a RightKit-owned capability. */
|
|
350
368
|
export function ownershipCheck(repoRoot, options = {}) {
|
|
351
369
|
let checked;
|
|
352
|
-
|
|
370
|
+
const split = options.releaseSource?.mode === "snapshot" ? options.releaseSource : null;
|
|
371
|
+
try { checked = (options.evidenceFactory ?? createOwnershipEvidence)(repoRoot, options.scanRoot ?? repoRoot, options.scope ?? null, ...(split ? [split] : [])); }
|
|
353
372
|
catch (error) { return fail("ownership", `ownership evidence failed: ${error.message}`, "restore protected RightKit ownership trust and catalog"); }
|
|
354
373
|
const { result, evidence, floor } = checked;
|
|
374
|
+
// R4: evidence for another commit or tree than the selected one is refused whatever produced it (a factory that read HEAD, a stale one).
|
|
375
|
+
if (split && (evidence?.commit !== split.commit || (evidence?.tree !== undefined && evidence.tree !== split.tree))) {
|
|
376
|
+
return fail("ownership", `ownership evidence is for commit ${String(evidence?.commit ?? "(none)").slice(0, 12)}, not the selected commit ${split.commit.slice(0, 12)}`, "the door run builds the selected commit's snapshot; re-run the door for that commit");
|
|
377
|
+
}
|
|
355
378
|
if (checked.ok) {
|
|
356
379
|
const excepted = result.covered.length;
|
|
357
380
|
const detail = excepted ? `no unapproved copies (${excepted} excepted findings)` : "no copies of RightKit-owned capabilities";
|
package/publish-cargo.mjs
CHANGED
|
@@ -397,9 +397,9 @@ export async function runCargoPublish(options, execute = (_command, args, settin
|
|
|
397
397
|
const proven = (reuseEvidence || options.trustEvidenceSteps === true) && !options.dryRun ? provenGateSteps(gate.evidence) : {};
|
|
398
398
|
if (reuseEvidence && (proven.tests !== true || proven.clippy !== true)) throw new Error("--from-evidence requires gate-proven tests and clippy for this crate and commit");
|
|
399
399
|
if (reuseEvidence) {
|
|
400
|
-
const status = (deps.workspaceStatus ?? ((workspaceRoot) => spawnSync("git", ["status", "--porcelain", "--",
|
|
400
|
+
const status = (deps.workspaceStatus ?? ((workspaceRoot) => spawnSync("git", ["status", "--porcelain", "--", `crates/${options.crate}/Cargo.toml`, `crates/${options.crate}/Cargo.lock`, "ci/gate.config.json", "ci/asset-gated.json"], { cwd: workspaceRoot, encoding: "utf8", windowsHide: true })))(options.workspaceRoot);
|
|
401
401
|
// The gate config decides the steps replayed below, so an uncommitted edit would change what is demanded (0.4.1 review A3).
|
|
402
|
-
if (status.error || status.status !== 0 || String(status.stdout ?? "").trim()) throw new Error("--from-evidence requires clean
|
|
402
|
+
if (status.error || status.status !== 0 || String(status.stdout ?? "").trim()) throw new Error("--from-evidence requires clean crate Cargo.toml, Cargo.lock and ci gate config (ci/gate.config.json, ci/asset-gated.json)");
|
|
403
403
|
const proofHost = singleHostOfPolicy(gate.evidence) ?? "darwin";
|
|
404
404
|
const runs = gate.evidence.runs.filter((run) => run?.host?.os === proofHost);
|
|
405
405
|
const command = `cargo package --locked -p ${options.crate}`;
|
|
@@ -480,7 +480,8 @@ export async function runCargoPublish(options, execute = (_command, args, settin
|
|
|
480
480
|
// `execute` may be synchronous (the default) or async (the batch publisher overlaps uploads).
|
|
481
481
|
const isReadOnlyInspection = ["package contents", "package location"].includes(step.label);
|
|
482
482
|
const settings = {
|
|
483
|
-
|
|
483
|
+
// Every crate is its own Cargo workspace with its own Cargo.lock and target directory (#1285): cargo runs in the crate directory.
|
|
484
|
+
cwd: crateDir,
|
|
484
485
|
encoding: "utf8",
|
|
485
486
|
windowsHide: true,
|
|
486
487
|
stdio: step.capture || (options.offlineReadOnly === true && isReadOnlyInspection) ? "pipe" : "inherit",
|
package/recipe-build.mjs
CHANGED
|
@@ -51,7 +51,7 @@ export class RecipeBuildError extends Error {
|
|
|
51
51
|
const fail = (code, message, details) => { throw new RecipeBuildError(code, message, details); };
|
|
52
52
|
const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
53
53
|
const STEP_KEYS = new Set(["phase", "cmd", "args", "cwd", "env"]);
|
|
54
|
-
const PLAN_KEYS = new Set(["version", "steps", "cleanup", "stability", "stage", "verifySidecars", "nativeHelpers"]);
|
|
54
|
+
const PLAN_KEYS = new Set(["version", "steps", "cleanup", "stability", "stage", "verifySidecars", "nativeHelpers", "boundTargets"]);
|
|
55
55
|
const VERIFY_SIDECARS_KEYS = new Set(["receipt", "target"]);
|
|
56
56
|
const NATIVE_HELPERS_KEYS = new Set(["app", "helpers", "version", "forceArch", "configuration"]);
|
|
57
57
|
const UNIVERSAL_TRIPLE = "universal-apple-darwin";
|
|
@@ -65,10 +65,22 @@ const unknownKeys = (value, allowed, label, errors) => { for (const key of Objec
|
|
|
65
65
|
const positiveInt = (value) => Number.isInteger(value) && value > 0;
|
|
66
66
|
const badVolumePrefix = (item) => typeof item !== "string" || !item.trim() || item === "." || item === ".." || item.includes("/") || item.includes("\\") || item.includes(":") || item.includes("\0");
|
|
67
67
|
|
|
68
|
-
/**
|
|
69
|
-
|
|
68
|
+
/**
|
|
69
|
+
* A relative path that stays inside the app root; returns the normalized form or undefined and pushes an error.
|
|
70
|
+
* `bound` (a door split, row 7.04c R-T2): the absolute Cargo target directories the plan binds. A path strictly inside one of them is also
|
|
71
|
+
* accepted, absolute and without `..`: the snapshot has no target directory, so a split's build output is addressed there. Any other
|
|
72
|
+
* absolute path stays refused.
|
|
73
|
+
*/
|
|
74
|
+
function insideApp(value, label, errors, { strict = false, bound = [] } = {}) {
|
|
70
75
|
if (typeof value !== "string" || !value.trim()) { errors.push(`${label} must be a non-empty relative path`); return undefined; }
|
|
71
76
|
const normalized = value.replaceAll("\\", "/");
|
|
77
|
+
if (bound.length && (path.isAbsolute(value) || /^[A-Za-z]:[\\/]/.test(value))) {
|
|
78
|
+
const parts = normalized.split("/");
|
|
79
|
+
const absolute = path.resolve(value);
|
|
80
|
+
const under = !parts.includes("..") && bound.some((dir) => { const relative = path.relative(dir, absolute); return relative !== "" && relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative); });
|
|
81
|
+
if (!under) { errors.push(`${label} is an absolute path outside every bound Cargo target (${bound.join(", ")}): ${value}`); return undefined; }
|
|
82
|
+
return normalized;
|
|
83
|
+
}
|
|
72
84
|
if (path.isAbsolute(value) || /^[A-Za-z]:/.test(value) || normalized.split("/").includes("..")) { errors.push(`${label} must be relative and stay inside the app: ${value}`); return undefined; }
|
|
73
85
|
const resolved = path.posix.normalize(normalized);
|
|
74
86
|
if (strict && (resolved === "." || resolved === "./" || resolved === "")) { errors.push(`${label} must name a path strictly inside the app, not the app root: ${JSON.stringify(value)}`); return undefined; }
|
|
@@ -76,21 +88,31 @@ function insideApp(value, label, errors, { strict = false } = {}) {
|
|
|
76
88
|
return normalized;
|
|
77
89
|
}
|
|
78
90
|
|
|
79
|
-
|
|
91
|
+
/** `boundTargets`: the absolute Cargo target directories of a door split (row 7.04c). Absent outside a split. */
|
|
92
|
+
function validateBoundTargets(value, errors) {
|
|
93
|
+
if (value === undefined) return undefined;
|
|
94
|
+
if (!Array.isArray(value) || !value.length || value.some((dir) => typeof dir !== "string" || !path.isAbsolute(dir) || dir.replaceAll("\\", "/").split("/").includes(".."))) {
|
|
95
|
+
errors.push("boundTargets must be a non-empty list of absolute directories without ..");
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
return value.map((dir) => path.resolve(dir));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function validateCleanup(value, errors, bound = []) {
|
|
80
102
|
if (value === undefined) return undefined;
|
|
81
103
|
if (!isObject(value)) { errors.push("cleanup must be an object"); return undefined; }
|
|
82
104
|
unknownKeys(value, CLEANUP_KEYS, "cleanup", errors);
|
|
83
105
|
const out = {};
|
|
84
106
|
if (value.remove !== undefined) {
|
|
85
107
|
if (!Array.isArray(value.remove)) errors.push("cleanup.remove must be a list of relative paths");
|
|
86
|
-
else out.remove = value.remove.map((item, index) => insideApp(item, `cleanup.remove[${index}]`, errors, { strict: true })).filter(Boolean);
|
|
108
|
+
else out.remove = value.remove.map((item, index) => insideApp(item, `cleanup.remove[${index}]`, errors, { strict: true, bound })).filter(Boolean);
|
|
87
109
|
}
|
|
88
110
|
if (value.apps !== undefined) {
|
|
89
111
|
if (!isObject(value.apps)) errors.push("cleanup.apps must be an object");
|
|
90
112
|
else {
|
|
91
113
|
unknownKeys(value.apps, APPS_KEYS, "cleanup.apps", errors);
|
|
92
114
|
for (const key of ["remove", "unregister"]) if (value.apps[key] !== undefined && typeof value.apps[key] !== "boolean") errors.push(`cleanup.apps.${key} must be a boolean`);
|
|
93
|
-
const dir = insideApp(value.apps.dir, "cleanup.apps.dir", errors, { strict: true });
|
|
115
|
+
const dir = insideApp(value.apps.dir, "cleanup.apps.dir", errors, { strict: true, bound });
|
|
94
116
|
if (dir) out.apps = { dir, remove: value.apps.remove === true, unregister: value.apps.unregister === true };
|
|
95
117
|
}
|
|
96
118
|
}
|
|
@@ -101,13 +123,13 @@ function validateCleanup(value, errors) {
|
|
|
101
123
|
return out;
|
|
102
124
|
}
|
|
103
125
|
|
|
104
|
-
function validateStability(value, errors) {
|
|
126
|
+
function validateStability(value, errors, bound = []) {
|
|
105
127
|
if (value === undefined) return undefined;
|
|
106
128
|
if (!isObject(value)) { errors.push("stability must be an object"); return undefined; }
|
|
107
129
|
unknownKeys(value, STABILITY_KEYS, "stability", errors);
|
|
108
130
|
for (const key of ["intervalMs", "checks", "timeoutMs"]) if (value[key] !== undefined && !positiveInt(value[key])) errors.push(`stability.${key} must be a positive integer`);
|
|
109
131
|
if (!Array.isArray(value.files) || !value.files.length) { errors.push("stability.files must be a non-empty list of relative paths"); return undefined; }
|
|
110
|
-
const files = value.files.map((item, index) => insideApp(item, `stability.files[${index}]`, errors, { strict: true })).filter(Boolean);
|
|
132
|
+
const files = value.files.map((item, index) => insideApp(item, `stability.files[${index}]`, errors, { strict: true, bound })).filter(Boolean);
|
|
111
133
|
return { files, ...(value.intervalMs === undefined ? {} : { intervalMs: value.intervalMs }), ...(value.checks === undefined ? {} : { checks: value.checks }), ...(value.timeoutMs === undefined ? {} : { timeoutMs: value.timeoutMs }) };
|
|
112
134
|
}
|
|
113
135
|
|
|
@@ -167,11 +189,11 @@ function validateVerifySidecars(value, errors) {
|
|
|
167
189
|
return receipt ? { receipt, target: UNIVERSAL_TRIPLE } : undefined;
|
|
168
190
|
}
|
|
169
191
|
|
|
170
|
-
function validateNativeHelpersSection(value, errors) {
|
|
192
|
+
function validateNativeHelpersSection(value, errors, bound = []) {
|
|
171
193
|
if (value === undefined) return undefined;
|
|
172
194
|
if (!isObject(value)) { errors.push("nativeHelpers must be { app, helpers, version?, forceArch?, configuration? }"); return undefined; }
|
|
173
195
|
unknownKeys(value, NATIVE_HELPERS_KEYS, "nativeHelpers", errors);
|
|
174
|
-
const app = insideApp(value.app, "nativeHelpers.app", errors, { strict: true });
|
|
196
|
+
const app = insideApp(value.app, "nativeHelpers.app", errors, { strict: true, bound });
|
|
175
197
|
if (app && !app.replace(/\/+$/, "").endsWith(".app")) errors.push("nativeHelpers.app must name the .app bundle");
|
|
176
198
|
let helpers;
|
|
177
199
|
try { helpers = validateNativeHelpers(value.helpers, { label: "nativeHelpers.helpers" }); } catch (error) { errors.push(...(error.details?.errors ?? [error.message])); }
|
|
@@ -208,13 +230,15 @@ export function validateRecipePlan(plan) {
|
|
|
208
230
|
steps.push({ phase: step.phase, cmd: step.cmd, args: Array.isArray(args) ? [...args] : [], ...(cwd ? { cwd } : {}), ...(isObject(step.env) ? { env: { ...step.env } } : {}) });
|
|
209
231
|
}
|
|
210
232
|
if (builds !== 1) errors.push(`exactly one build step is required, found ${builds}`);
|
|
211
|
-
const
|
|
212
|
-
const
|
|
233
|
+
const boundTargets = validateBoundTargets(plan.boundTargets, errors);
|
|
234
|
+
const cleanup = validateCleanup(plan.cleanup, errors, boundTargets ?? []);
|
|
235
|
+
const stability = validateStability(plan.stability, errors, boundTargets ?? []);
|
|
213
236
|
const stage = validateStage(plan.stage, errors);
|
|
214
237
|
const verifySidecars = validateVerifySidecars(plan.verifySidecars, errors);
|
|
215
|
-
const nativeHelpers = validateNativeHelpersSection(plan.nativeHelpers, errors);
|
|
238
|
+
const nativeHelpers = validateNativeHelpersSection(plan.nativeHelpers, errors, boundTargets ?? []);
|
|
239
|
+
if (boundTargets && stage && (stage.pre?.length || stage.post?.length)) errors.push("a plan that binds Cargo targets (a door split) plans no stage copies: the build output is read and signed in place (rightkit#924 copies are zero)");
|
|
216
240
|
if (errors.length) fail("RECIPE_BUILD_PLAN_INVALID", `invalid recipe plan:\n${errors.map((line) => `- ${line}`).join("\n")}`, { errors });
|
|
217
|
-
return { version: RECIPE_BUILD_PLAN_VERSION, steps, ...(cleanup ? { cleanup } : {}), ...(stability ? { stability } : {}), ...(stage ? { stage } : {}), ...(verifySidecars ? { verifySidecars } : {}), ...(nativeHelpers ? { nativeHelpers } : {}) };
|
|
241
|
+
return { version: RECIPE_BUILD_PLAN_VERSION, steps, ...(boundTargets ? { boundTargets } : {}), ...(cleanup ? { cleanup } : {}), ...(stability ? { stability } : {}), ...(stage ? { stage } : {}), ...(verifySidecars ? { verifySidecars } : {}), ...(nativeHelpers ? { nativeHelpers } : {}) };
|
|
218
242
|
}
|
|
219
243
|
|
|
220
244
|
/** This package's own right-release entry: the door's RightKit, whatever the app's node_modules holds. */
|