@rightkit/release 0.6.12 → 0.6.14
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 +49 -1
- package/adapter-env.mjs +165 -0
- package/android-ndk.mjs +5 -1
- package/android-release.mjs +73 -8
- package/app-check.mjs +22 -8
- package/app-closure.mjs +214 -0
- package/assemble-app-command.mjs +24 -7
- package/atomic-write.mjs +57 -0
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/rightkit-gate.mjs +1190 -335
- package/broker-client.mjs +106 -0
- package/broker-exception.mjs +211 -0
- package/broker-handshake.mjs +160 -0
- package/broker-pins.mjs +27 -8
- package/broker-wire.mjs +262 -0
- package/build-launch.mjs +364 -12
- package/build-ownership.mjs +117 -49
- package/build-release.mjs +3 -2
- package/cache-command.mjs +7 -2
- package/candidate-build-receipt.mjs +96 -0
- package/candidate-receipt-command.mjs +29 -0
- package/cargo-contract.mjs +4 -3
- package/cargo-ledger.mjs +1 -1
- package/cargo-license-notices.mjs +2 -2
- package/cargo-shapes.mjs +332 -0
- package/cargo-target-shim.mjs +44 -5
- package/cargo-target.mjs +5 -0
- package/carried-adapter-run.mjs +31 -0
- package/carried-cargo-run.mjs +9 -0
- package/carried-cargo.mjs +237 -0
- package/carrier-census.mjs +124 -0
- package/class-a-launch-guard.mjs +266 -33
- package/cli/right-release.mjs +2 -0
- package/clippy-route.mjs +23 -11
- package/compile-inputs.mjs +9 -12
- package/control-plane.mjs +18 -0
- package/deps-cargo-lock.mjs +208 -0
- package/deps.mjs +5 -0
- package/deterministic-scans.mjs +273 -44
- package/development-signing.mjs +14 -1
- package/development.mjs +109 -24
- package/direct-origin.mjs +107 -0
- package/disk-command.mjs +1 -0
- package/disk-policy.mjs +18 -3
- package/freshness-census.mjs +206 -58
- package/gate-executor.mjs +30 -12
- package/gate-stages.mjs +93 -16
- package/github-release.mjs +2 -1
- package/github-upload-gate.mjs +2 -1
- package/install-tauri-authenticode.mjs +346 -0
- package/ios-dev.mjs +77 -23
- package/ios-package-recipe.mjs +100 -41
- package/linux-artifacts.mjs +17 -2
- package/linux-qualifier.mjs +83 -0
- package/local-publish.mjs +36 -14
- package/local-runtime.mjs +2 -2
- package/lock-identity.mjs +138 -0
- package/lsclean.sh +0 -0
- package/managed-cargo.mjs +151 -44
- package/managed-roots.mjs +130 -12
- package/media-sidecar-catalog.mjs +1 -1
- package/native-adapters.mjs +411 -0
- package/native-helpers.mjs +26 -18
- package/native-tool-planning.mjs +94 -0
- package/ownership-mark.mjs +182 -0
- package/pack-digest.mjs +122 -0
- package/package.json +12 -12
- package/platform-dir.mjs +14 -0
- package/pnpm-adapter-grammar.mjs +226 -0
- package/progress-control.mjs +7 -2
- package/provenance-real-receipt.pins.json +7 -0
- package/provenance-verify.mjs +262 -1
- package/publication-outcomes.mjs +21 -0
- package/publish-update.mjs +2 -1
- package/qa-journeys-prebuilt-run.mjs +18 -0
- package/qa-journeys-prebuilt.mjs +148 -0
- package/qa-snapshot-run.mjs +53 -0
- package/qa-snapshot.mjs +164 -0
- package/qualification-verify.mjs +144 -0
- package/qualify-reuse.mjs +38 -0
- package/recipe-build.mjs +16 -3
- package/release-qa-audit.mjs +2 -2
- package/release-qa-command.mjs +2 -1
- package/release-qa-receipt.mjs +3 -2
- package/release-state.mjs +3 -2
- package/release.mjs +14 -3
- package/rightkit-drift.mjs +27 -5
- package/rightkit-versions.json +15 -10
- package/rustright-names.mjs +32 -0
- package/scan-tool-enrolment.mjs +167 -0
- package/sign-windows.mjs +212 -112
- package/signing-files.mjs +35 -0
- package/signing-identity.mjs +7 -0
- package/snapshot-broker.mjs +528 -57
- package/snapshot-check.mjs +95 -11
- package/snapshot-metadata.mjs +123 -0
- package/snapshot-request.mjs +389 -96
- package/stage-binary.mjs +22 -4
- package/stage-commands.mjs +56 -13
- package/stage-sidecar.mjs +93 -21
- package/suite-signing-policy.mjs +657 -0
- package/suite-signing.mjs +223 -0
- package/swift-scratch.mjs +25 -0
- package/tauri-locked.mjs +25 -0
- package/test-command.mjs +164 -29
- package/unit-gate.mjs +201 -31
- package/upload-large.mjs +0 -0
- package/web-build.mjs +2 -2
- package/win-package-recipe.mjs +26 -2
package/build-launch.mjs
CHANGED
|
@@ -7,47 +7,183 @@
|
|
|
7
7
|
// - cargo <args> -> <rustright> cargo <args> (by the resolved broker client, whatever path the caller named: a real
|
|
8
8
|
// cargo binary is never launched around the broker)
|
|
9
9
|
// - rustright <args> -> <rustright> <args> (the resolved broker client)
|
|
10
|
-
// - rightkit-qa <args> -> <rustright> run rightkit-qa -- <args> (the whole QA run, its test binaries included,
|
|
11
|
-
// job holding the app's claim
|
|
10
|
+
// - rightkit-qa <args> -> <rustright> run rightkit-qa -- <args> (off the snapshot route: the whole QA run, its test binaries included,
|
|
11
|
+
// is one broker job holding the app's claim). ON a snapshot stage it is NOT an adapter job: QA runs an
|
|
12
|
+
// already-built artifact and compiles nothing, so it stays a direct, gate-run step with its GUI session and
|
|
13
|
+
// unscrubbed environment. A `run-tests` step with no `--from-cargo-json` is two parts (qa-snapshot-run.mjs): a
|
|
14
|
+
// carried test build whose listing lands under the carrier's managed directory, then QA directly against
|
|
15
|
+
// it. A `journeys run` step is the same two parts (qa-journeys-prebuilt-run.mjs, GB-79: the gate carries the build, signs the products, hands QA the verified input) when rightkit-qa advertises `journeys-prebuilt-input`, else refused naming it. Any other QA step whose argv would build (`--build*`) is refused (`qaBuildReason`), as
|
|
16
|
+
// is a QA directory outside the snapshot or an input that resolves into the primary checkout (`qaPathReason`)
|
|
12
17
|
// - this package's right-release -> unchanged (same NODE_OPTIONS as this process): it orchestrates, and every build it starts comes back
|
|
13
18
|
// through this door
|
|
19
|
+
// A SNAPSHOT STAGE (the gate's carrier map is in the environment, RIGHTKIT_CARRIED_CARGO_MAP) carries every one of those steps itself: a
|
|
20
|
+
// `cargo` step, and a pnpm, node or swift step as a snapshot-bound adapter job (carried-adapter-run.mjs, rustright#190), one signed,
|
|
21
|
+
// ledgered request each, with the compiles of the step's children (the Tauri CLI) nested under it. A pnpm `install` step of a snapshot
|
|
22
|
+
// stage is skipped: the gate already installed the snapshot's dependencies.
|
|
23
|
+
// - swift <args> -> <rustright> run rightkit-swift -- <args> (a native helper's SwiftPM build; carried like the others on a snapshot stage)
|
|
24
|
+
// A carried adapter step's own environment (the `env` the step declares, passed as `host.stepEnv`) is forwarded as the adapter request's
|
|
25
|
+
// environment: the door sets RUSTRIGHT_ADAPTER_ENV to exactly those names (adapter-env.mjs), the broker accepts the names the adapter's
|
|
26
|
+
// registration lists in `env`, and the values are bound into the signed request. A reserved name is refused here; a step with no env gets no
|
|
27
|
+
// RUSTRIGHT_ADAPTER_ENV.
|
|
28
|
+
// - rustright run <adapter> -- <args> on a snapshot stage -> the same step as the adapter's own program (pnpm, node, rightkit-qa, swift), so
|
|
29
|
+
// the door's rules apply to it; any other `rustright <command>` there is refused (RK_BUILD_LAUNCH_REFUSED)
|
|
30
|
+
// - pnpm exec <target> -> only a target in EXEC_TARGETS (tauri, right-release, vite) on a broker host: the broker's pnpm entry
|
|
31
|
+
// has no target allowlist, so `pnpm exec node -e ...` would otherwise pass the node flag ban
|
|
14
32
|
// - anything else -> refused on a managed host: no broker adapter runs it yet, and running it directly would bypass the
|
|
15
33
|
// broker (fail closed; the per-app impact is mapped in rightkit#1241)
|
|
34
|
+
// - xcodegen | xcodebuild | gradle (./gradlew) | cmake -> the tool's own constrained adapter (native-adapters.mjs; RustRight's typed native grammar, rustright-policy native_adapters.rs): judged BEFORE the send by the
|
|
35
|
+
// registration, the broker's shape rules (modes, selections, output-moving -D entries) and its file rules (the Gradle wrapper's pin, the export options file); never an environment; a
|
|
36
|
+
// signing-properties file is a typed path handoff that a broker route refuses before launch (RK_NATIVE_SIGNING_FILE_UNSUPPORTED). The client of an uncarried one is the resolved
|
|
37
|
+
// executor's (the declared Linux broker's, else the managed runtime client), never a PATH lookup. `createNativeRunner` is the production runner the iOS and Android lanes inject.
|
|
16
38
|
// A launch from inside a broker job is routed the same way: the broker attributes it to its parent job by the parent's own
|
|
17
39
|
// capability token (never by an environment marker this module reads), so no launch here decides to skip the broker.
|
|
18
|
-
import { readFileSync } from "node:fs";
|
|
40
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
19
41
|
import path from "node:path";
|
|
20
42
|
import { fileURLToPath } from "node:url";
|
|
21
43
|
import { resolveExecutor } from "./gate-executor.mjs";
|
|
22
44
|
import { resolveExecutable } from "./admission-host.mjs";
|
|
45
|
+
import { CARRIED_CARGO_MAP_ENV as CARRIED_MAP_ENV, carriedCargoMapOf, runCarriedAdapter } from "./carried-cargo.mjs";
|
|
46
|
+
import { spawnSync } from "./background-process.mjs";
|
|
47
|
+
import { managedRuntimeClient } from "./managed-roots.mjs";
|
|
48
|
+
import { ADAPTER_ENV_MANIFEST, QA_HANDOFF_ENV, adapterEnvLaunch } from "./adapter-env.mjs";
|
|
49
|
+
import { cargoSubcommandOf, canonicalCargoCommand } from "./cargo-shapes.mjs";
|
|
50
|
+
import { swiftScratchRefusal } from "./swift-scratch.mjs";
|
|
51
|
+
import { NATIVE_ADAPTERS, NATIVE_SIGNING_FILE_ENV, nativeCallFsProblem, nativeManagedOutputs, nativeOutputsFromReceipt, nativeSigningFileRefusal, nativeStateDir, nativeToolLaunch, nativeToolOf } from "./native-adapters.mjs";
|
|
52
|
+
import { PNPM_ADAPTER_GRAMMAR, adapterArgvRefusal, liveAdapterGrammar, scanAdapterArgv } from "./pnpm-adapter-grammar.mjs";
|
|
53
|
+
import { isJourneysRun, journeysCapabilityRefusal, journeysRunRefusal, qaPrebuiltCapability } from "./qa-journeys-prebuilt.mjs";
|
|
54
|
+
import { QA_VALUE_OPTIONS, hasListing, isBuildFlag, ownWords, qaPathReason, qaSnapshotPlan } from "./qa-snapshot.mjs";
|
|
55
|
+
|
|
56
|
+
/** The environment variable the door sets for a stage that runs from a commit snapshot (gate-stages.mjs CHECKOUT_ENV). */
|
|
57
|
+
const SNAPSHOT_STAGE_ENV = "RK_GATE_CHECKOUT";
|
|
23
58
|
|
|
24
59
|
export const BUILD_LAUNCH_REFUSED = "RK_BUILD_LAUNCH_REFUSED";
|
|
60
|
+
/** The program name a sign step would have if it were a command: the door refuses it on every route. */
|
|
61
|
+
export const SIGN_STEP_COMMAND = "rightkit-sign-step";
|
|
25
62
|
const OWN_RIGHT_RELEASE = fileURLToPath(new URL("./cli/right-release.mjs", import.meta.url));
|
|
63
|
+
// GB-06: the helpers the package recipes generate (package-recipe-targets.mjs) are this package's own release operations, run by this very Node binary at their exact installed
|
|
64
|
+
// paths, like right-release itself: they orchestrate in the gate's own session, and every build-capable child they start comes back through this door as a carried step.
|
|
65
|
+
// Only these exact files qualify; an edited app or a copy under its node_modules is a different path and is refused like any other script outside the job's workspace.
|
|
66
|
+
const RECIPE_BUILD_HELPER = fileURLToPath(new URL("./recipe-build.mjs", import.meta.url));
|
|
67
|
+
const CREATE_MAC_UPDATER_HELPER = fileURLToPath(new URL("./create-mac-updater.mjs", import.meta.url));
|
|
68
|
+
export const INTERNAL_RELEASE_HELPERS = Object.freeze([OWN_RIGHT_RELEASE, RECIPE_BUILD_HELPER, CREATE_MAC_UPDATER_HELPER]);
|
|
69
|
+
const CARRIED_RUN = fileURLToPath(new URL("./carried-cargo-run.mjs", import.meta.url));
|
|
70
|
+
const CARRIED_ADAPTER_RUN = fileURLToPath(new URL("./carried-adapter-run.mjs", import.meta.url));
|
|
71
|
+
const QA_SNAPSHOT_RUN = fileURLToPath(new URL("./qa-snapshot-run.mjs", import.meta.url));
|
|
72
|
+
const QA_JOURNEYS_RUN = fileURLToPath(new URL("./qa-journeys-prebuilt-run.mjs", import.meta.url));
|
|
73
|
+
// pnpm options that take the next argument as their value (so that value is not the command).
|
|
74
|
+
const PNPM_VALUE_OPTIONS = new Set(["--dir", "-C", "--filter", "-F", "--prefix", "--config", "--reporter", "--workspace-concurrency"]);
|
|
26
75
|
// Node flags that change no code path; everything else before the script (-e, --eval, -p, --import, -r, --require, --loader, …) refuses.
|
|
27
76
|
const NODE_FLAGS = /^--(?:disable-warning=[A-Za-z0-9_-]+|no-experimental-webstorage|no-warnings|enable-source-maps|max-old-space-size=\d+|stack-size=\d+)$/;
|
|
77
|
+
/** The pnpm `exec` targets the door admits (every app config's `pnpm exec <target>`: tauri and right-release; vite for a web build). */
|
|
78
|
+
export const EXEC_TARGETS = Object.freeze(["tauri", "right-release", "vite"]);
|
|
79
|
+
/** The broker adapter a `rustright run <adapter> -- ...` names, and the program whose door rules judge the step. */
|
|
80
|
+
const ADAPTER_PROGRAMS = Object.freeze({ "rightkit-pnpm": "pnpm", "rightkit-node": "node", "rightkit-qa": "rightkit-qa", "rightkit-swift": "swift", "rightkit-xcodegen": "xcodegen", "rightkit-xcodebuild": "xcodebuild", "rightkit-gradle": "gradle", "rightkit-cmake": "cmake" });
|
|
81
|
+
/** The tools a QA step must not start: running one is a build, and a QA step compiles nothing. */
|
|
82
|
+
const QA_BUILD_PROGRAMS = new Set(["cargo", "rustc", "rustdoc", "rustright", "rightkit", "pnpm", "npm", "npx", "yarn", "swift", "swiftc", "xcodebuild", "gradle", "gradlew", "make", "cmake", "node-gyp"]);
|
|
83
|
+
export { QA_VALUE_OPTIONS };
|
|
84
|
+
/**
|
|
85
|
+
* Why a `rightkit-qa` argv would build, or null. A QA step on a snapshot stage runs an already-built artifact: `run-tests` compiles its
|
|
86
|
+
* test binaries unless it is handed an existing listing (`--from-cargo-json`, read in flag position only), `journeys run` builds through the
|
|
87
|
+
* broker, `run --build`, `--build-ui` and `--build-with` build, and any named toolchain command is a build. `carried` says the gate carries a
|
|
88
|
+
* `run-tests` step's build itself (qa-snapshot.mjs), so that one is not reported; `journeys run` has no prebuilt form and always is.
|
|
89
|
+
*/
|
|
90
|
+
export function qaBuildReason(args, { carried = false } = {}) {
|
|
91
|
+
const words = args.map(String);
|
|
92
|
+
// run-tests has its own parser (words after `--` are test arguments); every other command's Args reads the whole argv (rightkit-qa.rs
|
|
93
|
+
// Args::has / value ignore `--`), so for those a build flag or tool anywhere is the build.
|
|
94
|
+
const own = words[0] === "run-tests" ? ownWords(words) : words;
|
|
95
|
+
if (words[0] === "run-tests" && !hasListing(words) && !carried) return "`rightkit-qa run-tests` without --from-cargo-json compiles its test binaries (`cargo test --no-run`)";
|
|
96
|
+
// GB-79: `journeys run` spawns its own `rustright cargo ... --no-run`. On a gate-carried stage the gate carries that build itself and hands QA the verified, signed input
|
|
97
|
+
// (qa-journeys-prebuilt.mjs, capability journeys-prebuilt-input), so there it is not a build; anywhere else it is, and a listing or attestation the step names is unverified input.
|
|
98
|
+
if (words[0] === "journeys" && words[1] === "run" && !carried) return "`rightkit-qa journeys run` builds through the broker before it launches (it spawns its own `rustright cargo ... --no-run`); only a gate-carried stage runs it, on the gate's own verified prebuilt input (capability journeys-prebuilt-input)";
|
|
99
|
+
if (words[0] === "journeys" && words[1] === "run") { const own = journeysRunRefusal(words); if (own) return own; }
|
|
100
|
+
if (own.some(isBuildFlag)) return "a --build, --build-ui or --build-with flag makes the QA run build";
|
|
101
|
+
const tool = own.find((w) => QA_BUILD_PROGRAMS.has(name(w)));
|
|
102
|
+
if (tool !== undefined) return `the argument ${JSON.stringify(tool)} names a build tool (${name(tool)}); a QA step compiles nothing`;
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
28
105
|
const name = (program) => path.basename(String(program ?? "").replaceAll("\\", "/")).replace(/\.(exe|cmd|bat)$/i, "").toLowerCase();
|
|
29
106
|
|
|
30
|
-
function refuse(message) {
|
|
107
|
+
function refuse(message, reason) {
|
|
31
108
|
const error = new Error(message);
|
|
32
109
|
error.code = BUILD_LAUNCH_REFUSED;
|
|
110
|
+
if (reason !== undefined) error.reason = reason;
|
|
33
111
|
throw error;
|
|
34
112
|
}
|
|
35
113
|
|
|
114
|
+
/** Why a `cargo ndk` output directory (`-o` / `--output-dir`) is one the broker refuses on its text alone (empty, the directory itself, anywhere inside `.git`), or null. Links and the workspace root are the broker's to judge. */
|
|
115
|
+
export function cargoNdkOutputProblem(words) {
|
|
116
|
+
const sub = words.find((word) => !word.startsWith("-"));
|
|
117
|
+
if (sub !== "ndk") return null;
|
|
118
|
+
for (let i = 0; i < words.length; i += 1) {
|
|
119
|
+
const word = words[i];
|
|
120
|
+
const value = word === "-o" || word === "--output-dir" ? words[i + 1] : word.startsWith("--output-dir=") ? word.slice("--output-dir=".length) : undefined;
|
|
121
|
+
if (value === undefined) continue;
|
|
122
|
+
if (value === "") return "-o is empty, not a directory of the workspace";
|
|
123
|
+
const parts = value.split(/[\\/]/).filter((part) => part !== "" && part !== ".");
|
|
124
|
+
if (parts.length === 0) return `-o ${value} is the working directory itself, not a directory in it`;
|
|
125
|
+
if (parts.some((part) => part.toLowerCase() === ".git")) return `-o ${value} is inside .git`;
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The index of the pnpm command in an argv (the first word that is not an option or an option's value), or -1. */
|
|
131
|
+
function pnpmCommandAt(words) {
|
|
132
|
+
for (let i = 0; i < words.length; i += 1) {
|
|
133
|
+
const w = words[i];
|
|
134
|
+
if (w === "--") return -1;
|
|
135
|
+
if (w.startsWith("-")) { if (PNPM_VALUE_OPTIONS.has(w)) i += 1; continue; }
|
|
136
|
+
return i;
|
|
137
|
+
}
|
|
138
|
+
return -1;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The pnpm command of an argv read LENIENTLY (the first word that is not an option or an option's value), or null. Only the install-skip of a snapshot
|
|
143
|
+
* stage uses it (a stage that skips `install` runs nothing, so a lenient reading can only skip more); everything the door SENDS is judged by the
|
|
144
|
+
* registration's own parser (pnpm-adapter-grammar.mjs scanAdapterArgv, GB-03).
|
|
145
|
+
*/
|
|
146
|
+
export function pnpmCommandOf(args) {
|
|
147
|
+
const words = args.map(String);
|
|
148
|
+
const at = pnpmCommandAt(words);
|
|
149
|
+
return at < 0 ? null : words[at];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Whether this environment belongs to a snapshot stage: the gate's carrier map or its source split is in it. */
|
|
153
|
+
export const onSnapshotRoute = (env) => Boolean(env?.[CARRIED_MAP_ENV] || env?.[SNAPSHOT_STAGE_ENV]);
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Why a pnpm step is skipped on the snapshot route, or null: an `install` step (`pnpm install`, `pnpm --dir hub install --frozen-lockfile`)
|
|
157
|
+
* would write into the commit's snapshot after the gate installed its dependencies, and the broker's pnpm adapter refuses it there. The
|
|
158
|
+
* gate's own install is the one the stage runs on (snapshot-check.mjs installSnapshotDependencies).
|
|
159
|
+
*/
|
|
160
|
+
export function snapshotInstallSkip(command, args, env) {
|
|
161
|
+
if (name(command) !== "pnpm" || !onSnapshotRoute(env)) return null;
|
|
162
|
+
const sub = pnpmCommandOf(args);
|
|
163
|
+
return sub === "install" || sub === "i" ? `pnpm ${args.join(" ")}: the gate already installed this snapshot's dependencies` : null;
|
|
164
|
+
}
|
|
165
|
+
|
|
36
166
|
/**
|
|
37
167
|
* `{ command, args }` to launch for a build or test command, or a refusal. `host` carries env, platform, exists, which and readFile
|
|
38
168
|
* (resolveExecutor's inputs); `which` defaults to a PATH lookup over `env`.
|
|
39
169
|
*/
|
|
40
170
|
export function buildLaunch(command, args = [], host = {}) {
|
|
171
|
+
// A `sign` step of tests.suites (suite-signing.mjs) runs in-process in the gate's own session: it is no command, no broker job and never carried.
|
|
172
|
+
if (command === SIGN_STEP_COMMAND || host.signStep) refuse("a sign step runs in-process in the gate's own session (signing agent, Keychain and Azure signer are the session's, not a broker job's); it never reaches the carrier (rightkit#1303)");
|
|
41
173
|
const env = host.env ?? process.env;
|
|
42
174
|
const platform = host.platform ?? process.platform;
|
|
43
175
|
const exists = host.exists;
|
|
44
176
|
const lookup = (cmd) => { const found = resolveExecutable(cmd, { env, hostPlatform: platform, exists: exists ?? (() => false) }); return found ? [found] : []; };
|
|
177
|
+
// A snapshot stage never installs into the snapshot (the gate did): the step is a logged no-op, on every host.
|
|
178
|
+
if (snapshotInstallSkip(command, args, env)) return { command: process.execPath, args: [CARRIED_ADAPTER_RUN, "--skip-install", ...args.map(String)] };
|
|
45
179
|
const executor = resolveExecutor({ env, platform, exists, which: host.which ?? lookup, readFile: host.readFile ?? ((file) => readFileSync(file, "utf8")) });
|
|
46
180
|
if (executor.kind === "refuse") refuse(`${name(command)} ${args.join(" ")}: ${executor.problem}`);
|
|
47
181
|
// A direct (Linux) host: the real toolchain, RustRight shim directories off PATH (`env`, to merge into the launch env); a configured
|
|
48
182
|
// `rustright cargo|rustc|rustdoc …` runs that tool directly; any other `rustright` command has no direct form and is refused.
|
|
49
183
|
if (executor.kind === "direct") {
|
|
50
|
-
|
|
184
|
+
// RA-08: a direct host has no broker, so a native tool (Gradle) runs in the gate's own session, and the signing-properties file's PATH (never its contents) reaches it in its environment, as it always did.
|
|
185
|
+
const signingEnv = host.signingFile && nativeToolOf(command) !== null ? { [NATIVE_SIGNING_FILE_ENV]: String(host.signingFile) } : null;
|
|
186
|
+
const extra = executor.env || signingEnv ? { env: { ...(executor.env ?? {}), ...(signingEnv ?? {}) } } : {};
|
|
51
187
|
if (name(command) === "rustright") {
|
|
52
188
|
if (!["cargo", "rustc", "rustdoc"].includes(String(args[0]))) refuse(`rustright ${args.join(" ")}: no broker on this ${platform} host, and only rustright cargo|rustc|rustdoc map to the toolchain`);
|
|
53
189
|
return { command: String(args[0]), args: args.slice(1).map(String), ...extra };
|
|
@@ -55,15 +191,144 @@ export function buildLaunch(command, args = [], host = {}) {
|
|
|
55
191
|
return { command, args, ...extra };
|
|
56
192
|
}
|
|
57
193
|
const program = name(command);
|
|
194
|
+
const handoffEnv = () => {
|
|
195
|
+
const found = {};
|
|
196
|
+
for (const key of QA_HANDOFF_ENV) if (typeof env[key] === "string" && env[key] !== "") found[key] = env[key];
|
|
197
|
+
return found;
|
|
198
|
+
};
|
|
199
|
+
// GB-05: a Cargo suite consumer (`cargo test|bench|run|nextest`) receives the signed handoff names the same way, through RUSTRIGHT_FORWARD_ENV
|
|
200
|
+
// (the client forwards the caller's value for each name, for these runtime commands only; the repository declares both names in `forwardEnv` of its
|
|
201
|
+
// `.rustright/broker.json`, or the host lists them in `buildRules.forwardTestEnv`). Nothing here carries a signing key or credential.
|
|
202
|
+
const cargoHandoffEnv = (cargoArgs) => {
|
|
203
|
+
const present = Object.keys(handoffEnv());
|
|
204
|
+
if (!present.length || !["test", "bench", "run", "nextest"].includes(canonicalCargoCommand(cargoSubcommandOf(cargoArgs.map(String))))) return {};
|
|
205
|
+
for (const key of present) {
|
|
206
|
+
if (!(path.isAbsolute(env[key]) || path.win32.isAbsolute(env[key])) || env[key].split(/[\\/]/).includes("..")) refuse(`${name(command)} ${args.join(" ")}: ${key} must be an absolute path with no '..' (the signed artifact's exact path); it is not forwarded`);
|
|
207
|
+
}
|
|
208
|
+
const existing = String(env.RUSTRIGHT_FORWARD_ENV ?? "").split(",").map((n) => n.trim()).filter(Boolean);
|
|
209
|
+
return { env: { RUSTRIGHT_FORWARD_ENV: [...new Set([...existing, ...present])].join(",") } };
|
|
210
|
+
};
|
|
211
|
+
// A snapshot stage's `cargo` and `rustright cargo` steps are carried (carried-cargo.mjs), never sent to the client: one signed, ledgered
|
|
212
|
+
// request per call, from the step's working directory, on every platform (rightkit#1301 F3).
|
|
213
|
+
const carriedArgs = program === "cargo" ? args : program === "rustright" && String(args[0]) === "cargo" ? args.slice(1) : null;
|
|
214
|
+
// R3-X02: the broker's cargo-ndk output rule (rustright-policy ndk.rs `check_output_dir`): the libraries go to a directory strictly inside the workspace, never `.git`. The lexical half is judged here, before the send.
|
|
215
|
+
const ndkProblem = carriedArgs ? cargoNdkOutputProblem(carriedArgs.map(String)) : null;
|
|
216
|
+
if (ndkProblem) refuse(`RK_NDK_OUTPUT_DENIED: cargo ${carriedArgs.join(" ")}: ${ndkProblem}; the broker's cargo-ndk output check would refuse it, so it is not sent`, "RK_NDK_OUTPUT_DENIED");
|
|
217
|
+
const carried = carriedArgs ? carriedCargoMapOf(env) : null;
|
|
218
|
+
// A carrier map that cannot be read is a refusal, never a fallback to the uncarried client (rightkit#1301 M1).
|
|
219
|
+
if (carried?.error) refuse(`${name(command)} ${args.join(" ")}: ${carried.error}; a snapshot stage's Cargo step is never sent to the client around its carrier`);
|
|
220
|
+
if (carriedArgs && carried?.map) return { command: process.execPath, args: [CARRIED_RUN, ...carriedArgs.map(String)], ...cargoHandoffEnv(carriedArgs) };
|
|
58
221
|
const client = () => lookup("rustright")[0] ?? refuse(`rustright is not on PATH: on a broker-managed host ${program} runs only as a broker job`);
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (
|
|
222
|
+
// A snapshot stage's pnpm, node and swift steps are carried adapter jobs (carried-adapter-run.mjs); anywhere else they are the client's `run`.
|
|
223
|
+
// A QA step is not one of them: it runs an already-built artifact (see the rightkit-qa branch below).
|
|
224
|
+
const adapterMap = ["pnpm", "node", "swift"].includes(program) || nativeToolOf(command) !== null ? carriedCargoMapOf(env) : null;
|
|
225
|
+
if (adapterMap?.error) refuse(`${name(command)} ${args.join(" ")}: ${adapterMap.error}; a snapshot stage's step is never sent to the client around its carrier`);
|
|
226
|
+
// The step's own environment rides the adapter request (adapter-env.mjs): RUSTRIGHT_ADAPTER_ENV names exactly what the step declares, plus the
|
|
227
|
+
// signed-artifact handoff (GB-05) when a sign step set it in this environment. Every name is judged against the adapter's registered `env`
|
|
228
|
+
// (the live registration, else the candidate manifest) BEFORE launch (GB-04): an unregistered key refuses, naming it and the registered set.
|
|
229
|
+
const stepEnvLaunch = (adapter) => {
|
|
230
|
+
try {
|
|
231
|
+
const registered = host.registeredEnv?.[adapter] ?? liveAdapterGrammar({ env, platform, readFile: host.readFile, adapter })?.env ?? ADAPTER_ENV_MANIFEST[adapter] ?? [];
|
|
232
|
+
return adapterEnvLaunch({ ...(host.stepEnv ?? {}), ...handoffEnv() }, { registered });
|
|
233
|
+
} catch (error) {
|
|
234
|
+
const refusal = new Error(`${name(command)} ${args.join(" ")}: ${error.message}`);
|
|
235
|
+
refusal.code = BUILD_LAUNCH_REFUSED;
|
|
236
|
+
refusal.reason = error.code;
|
|
237
|
+
throw refusal;
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
// The client a NATIVE launch uses is the resolved executor's, never a PATH lookup: the declared Linux broker's validated client (executor.client), else the installed managed runtime client of a
|
|
241
|
+
// Mac or Dell host (managedRuntimeClient: the live runtime at its final RustRight path, the control root's, the pre-cutover link). A `rustright` on PATH is a name the executor choice noticed, not an
|
|
242
|
+
// identity: nothing is launched until the exact client is found, and none is found by searching PATH.
|
|
243
|
+
const executorClient = () => executor.client ?? managedRuntimeClient(env, platform, exists) ?? refuse(`${name(command)} ${args.join(" ")}: no installed RustRight client for this broker-managed ${platform} host (the declared Linux broker client, or the managed runtime client); a native build tool is never launched through a PATH lookup`, "RK_NATIVE_CLIENT_UNRESOLVED");
|
|
244
|
+
const adapterLaunch = (adapter, adapterArgs, resolveClient = client) => {
|
|
245
|
+
const stepLaunchEnv = stepEnvLaunch(adapter);
|
|
246
|
+
const launchEnv = { ...(Object.keys(stepLaunchEnv).length ? { env: stepLaunchEnv } : {}), ...(stepLaunchEnv.signingEnvDropped?.length ? { signingEnvDropped: stepLaunchEnv.signingEnvDropped } : {}) };
|
|
247
|
+
return adapterMap?.map ? { command: process.execPath, args: [CARRIED_ADAPTER_RUN, adapter, "--", ...adapterArgs], ...launchEnv } : { command: resolveClient(), args: ["run", adapter, "--", ...adapterArgs], ...launchEnv };
|
|
248
|
+
};
|
|
249
|
+
if (program === "cargo") return { command: client(), args: ["cargo", ...args.map(String)], ...cargoHandoffEnv(args) };
|
|
250
|
+
if (program === "rustright") {
|
|
251
|
+
const snapshotMap = carriedCargoMapOf(env);
|
|
252
|
+
if (!snapshotMap) {
|
|
253
|
+
// GB-27: the client spelling of the same unregistered adapter is refused as well (no snapshot carrier, no rightkit-qa route).
|
|
254
|
+
if (args[0] === "run" && String(args[1]) === "rightkit-qa") refuse(`rustright ${args.join(" ")}: a QA run is a direct, noncompiling step of a snapshot stage under the door; the broker registers no rightkit-qa adapter, so outside a snapshot stage there is no uncarried route`);
|
|
255
|
+
return { command: client(), args: args.map(String) };
|
|
256
|
+
}
|
|
257
|
+
if (snapshotMap.error) refuse(`rustright ${args.join(" ")}: ${snapshotMap.error}; a snapshot stage's step is never sent to the client around its carrier`);
|
|
258
|
+
// A snapshot stage: `rustright run <adapter> -- <args>` is that adapter's step (judged by the door's rules for its program, then carried);
|
|
259
|
+
// any other `rustright <command>` is an uncarried client call that no registered adapter takes, so it is refused, naming the step.
|
|
260
|
+
const adapterProgram = args[0] === "run" && args[2] === "--" ? ADAPTER_PROGRAMS[String(args[1])] : undefined;
|
|
261
|
+
if (!adapterProgram) refuse(`rustright ${args.join(" ")}: a snapshot stage carries every compile, and this client call maps to no registered adapter (rustright run rightkit-pnpm|rightkit-node|rightkit-qa|rightkit-swift -- <args>); it is never sent to the client uncarried`);
|
|
262
|
+
return buildLaunch(adapterProgram, args.slice(3), host);
|
|
263
|
+
}
|
|
264
|
+
if (program === "rightkit-qa") {
|
|
265
|
+
// On a snapshot stage QA is a direct, gate-run step against the already-built artifact: no broker job (a QA run needs the GUI session and
|
|
266
|
+
// the unscrubbed environment the broker's job lacks), and nothing in it may compile (the broker refuses an uncarried compile from the snapshot).
|
|
267
|
+
const snapshotMap = carriedCargoMapOf(env);
|
|
268
|
+
if (snapshotMap?.error) refuse(`rightkit-qa ${args.join(" ")}: ${snapshotMap.error}; a snapshot stage's step is never run around its carrier`);
|
|
269
|
+
// GB-27 (GATE one entry): the broker registers no `rightkit-qa` adapter, so a QA step with no snapshot carrier has no route: it is
|
|
270
|
+
// refused here, naming the step, never sent as `rustright run rightkit-qa` (an unregistered capability the broker would refuse after the
|
|
271
|
+
// gate had marked the call submitted). QA runs only as the direct, noncompiling step of a snapshot stage under the door.
|
|
272
|
+
if (!snapshotMap) refuse(`rightkit-qa ${args.join(" ")}: a QA run is a direct, noncompiling step of a snapshot stage under the door; the broker registers no rightkit-qa adapter, so outside a snapshot stage there is no uncarried route (and any build the run needs is its own carried build step)`);
|
|
273
|
+
// The step runs from the snapshot and reads only the snapshot and managed output: its directory is inside the snapshot, and nothing it
|
|
274
|
+
// names (the listing, --root, --cwd, --manifest-path) resolves into the primary checkout.
|
|
275
|
+
const section = snapshotMap.map.broker ?? {};
|
|
276
|
+
const checkouts = [section.checkout, env[SNAPSHOT_STAGE_ENV]].filter((c) => typeof c === "string" && c);
|
|
277
|
+
const placed = qaPathReason(args, { cwd: host.cwd ?? process.cwd(), snapshot: section.snapshot?.path, checkouts, env: { ...env, ...host.stepEnv } });
|
|
278
|
+
if (placed) refuse(`rightkit-qa ${args.join(" ")}: ${placed}`);
|
|
279
|
+
const reason = qaBuildReason(args, { carried: true });
|
|
280
|
+
if (reason) refuse(`rightkit-qa ${args.join(" ")}: ${reason}; a QA step of a snapshot stage runs the already-built artifact and compiles nothing, so a build it needs is its own carried build step (a test build with --no-run --message-format=json, then run-tests --from-cargo-json)`);
|
|
281
|
+
const qa = lookup("rightkit-qa")[0] ?? "rightkit-qa";
|
|
282
|
+
// `run-tests` with no listing: the gate carries its test build, then runs QA directly against the listing (qa-snapshot-run.mjs).
|
|
283
|
+
// GB-79: a `journeys run` step is the gate's build-once route (carry the build, sign the products, hand QA the verified input) when QA implements the prebuilt input.
|
|
284
|
+
if (isJourneysRun(args)) {
|
|
285
|
+
const capable = (host.qaPrebuiltCapability ?? ((binary) => qaPrebuiltCapability(binary, { env: { ...env, ...host.stepEnv } })))(qa);
|
|
286
|
+
if (!capable) refuse(journeysCapabilityRefusal(`rightkit-qa ${args.join(" ")}`));
|
|
287
|
+
return { command: process.execPath, args: [QA_JOURNEYS_RUN, qa, "--", ...args.map(String)] };
|
|
288
|
+
}
|
|
289
|
+
const plan = qaSnapshotPlan(args);
|
|
290
|
+
if (plan?.refusal) refuse(`rightkit-qa ${args.join(" ")}: ${plan.refusal}`);
|
|
291
|
+
if (plan) return { command: process.execPath, args: [QA_SNAPSHOT_RUN, qa, "--", ...args.map(String)] };
|
|
292
|
+
return { command: qa, args: args.map(String) };
|
|
293
|
+
}
|
|
294
|
+
// GB-51 / RA-07..RA-09: a declared native build tool (xcodegen, xcodebuild, Gradle and its wrapper, CMake) is a snapshot-bound adapter job of its own constrained adapter (native-adapters.mjs): its argv is
|
|
295
|
+
// judged by the registration and by the broker's own shape and file rules BEFORE the send (an output override, an unregistered flag or a shape the broker has no managed form for is refused here, naming
|
|
296
|
+
// the capability), paths are made relative to the step's directory, and the step carries no environment of its own: the signing-properties file is a typed handoff, never an environment, and refuses
|
|
297
|
+
// here when no broker can carry it. The client is the resolved executor's. Nothing runs the tool around the broker, and no generic spawn stands in for a compiler-capable phase.
|
|
298
|
+
const nativeTool = nativeToolOf(command);
|
|
299
|
+
if (nativeTool !== null) {
|
|
300
|
+
const entry = NATIVE_ADAPTERS[nativeTool];
|
|
301
|
+
const cwd = host.cwd ?? process.cwd();
|
|
302
|
+
const advertised = host.brokerCapabilities;
|
|
303
|
+
if (advertised !== undefined && advertised !== null && !(advertised instanceof Set ? advertised : new Set(advertised)).has(entry.capability)) refuse(`${name(command)} ${args.join(" ")}: the broker does not advertise ${entry.capability}; nothing was sent`, "RK_NATIVE_CAPABILITY_MISSING");
|
|
304
|
+
const launch = nativeToolLaunch(nativeTool, args, { cwd });
|
|
305
|
+
if (launch.refusal) refuse(`${launch.refusal.code}: ${launch.refusal.message}`, launch.refusal.code);
|
|
306
|
+
const fsProblem = nativeCallFsProblem(nativeTool, launch.args, { cwd, ...(host.nativeFs ? { fsOps: host.nativeFs } : {}) });
|
|
307
|
+
if (fsProblem) {
|
|
308
|
+
const code = fsProblem.pin ? "RK_ADAPTER_PIN_REQUIRED" : "RK_ADAPTER_ARGV_DENIED";
|
|
309
|
+
refuse(`${code}: ${nativeTool} ${launch.args.join(" ")}: ${fsProblem.message}; the broker's ${entry.adapter} adapter (capability ${entry.capability}) would refuse it (rightkit#1241), so it is not sent`, code);
|
|
310
|
+
}
|
|
311
|
+
const signing = nativeSigningFileRefusal(nativeTool, host.signingFile, { signerRoot: host.signerRoot });
|
|
312
|
+
if (signing) refuse(`${signing.code}: ${signing.message}`, signing.code);
|
|
313
|
+
const launchedBare = adapterLaunch(entry.adapter, launch.args, executorClient);
|
|
314
|
+
// The declared Linux broker's client talks to the endpoint the route validated (executor.env), not to whatever the environment names.
|
|
315
|
+
const launched = !adapterMap?.map && executor.env ? { ...launchedBare, env: { ...(launchedBare.env ?? {}), ...executor.env } } : launchedBare;
|
|
316
|
+
// What the caller needs to map the receipt back: the tool, the adapter, the argv as SENT and the step's snapshot-relative directory (the broker keys its per-project state by it).
|
|
317
|
+
const snapshotRoot = adapterMap?.map?.broker?.snapshot?.path;
|
|
318
|
+
const real = (dir) => { try { return realpathSync(dir); } catch { return path.resolve(dir); } };
|
|
319
|
+
const relativeCwd = typeof snapshotRoot === "string" && snapshotRoot ? path.relative(real(snapshotRoot), real(cwd)).split(path.sep).join("/") : null;
|
|
320
|
+
return { ...launched, native: { tool: nativeTool, adapter: entry.adapter, capability: entry.capability, args: launch.args, relativeCwd, carried: Boolean(adapterMap?.map) } };
|
|
321
|
+
}
|
|
322
|
+
if (program === "swift") {
|
|
323
|
+
// GB-67: the broker owns SwiftPM's scratch and build path; a caller that names one is refused here, typed, before anything is sent.
|
|
324
|
+
const owned = swiftScratchRefusal(args.map(String));
|
|
325
|
+
if (owned) refuse(`${owned.code}: ${owned.message}`);
|
|
326
|
+
return adapterLaunch("rightkit-swift", args.map(String));
|
|
327
|
+
}
|
|
63
328
|
// This package's own right-release, run by this very Node binary (not any executable named node), with no startup code added by
|
|
64
329
|
// the caller: NODE_OPTIONS is empty or exactly this process's own string (a hook's env could otherwise --import code that builds
|
|
65
330
|
// outside the broker; reordered tokens can re-pair an option with another value, so nothing short of equality is accepted).
|
|
66
|
-
if (command === process.execPath && path.resolve(String(args[0] ?? ""))
|
|
331
|
+
if (command === process.execPath && INTERNAL_RELEASE_HELPERS.includes(path.resolve(String(args[0] ?? "")))) {
|
|
67
332
|
const options = String(env.NODE_OPTIONS ?? "");
|
|
68
333
|
if (options.trim() !== "" && options !== String(process.env.NODE_OPTIONS ?? "")) refuse(`right-release ${args.slice(1).join(" ")}: NODE_OPTIONS is neither empty nor exactly this process's own; on a broker-managed host the door's own right-release runs with no added startup code (rightkit#1241)`);
|
|
69
334
|
return { command, args };
|
|
@@ -79,10 +344,97 @@ export function buildLaunch(command, args = [], host = {}) {
|
|
|
79
344
|
const cwd = host.cwd ?? process.cwd();
|
|
80
345
|
const script = path.isAbsolute(String(args[at])) ? path.relative(cwd, String(args[at])) : String(args[at]);
|
|
81
346
|
if (!script || script.startsWith("..") || path.isAbsolute(script)) refuse(`node ${args[0]}: the script is outside the job's workspace (${cwd}); on a broker-managed host node runs only workspace scripts as broker jobs (rightkit-node, rightkit#1241)`);
|
|
82
|
-
return
|
|
347
|
+
return adapterLaunch("rightkit-node", [...flags, script.replaceAll("\\", "/"), ...args.slice(at + 1).map(String)]);
|
|
83
348
|
}
|
|
84
349
|
if (program === "pnpm") {
|
|
85
|
-
|
|
350
|
+
const words = args.map(String);
|
|
351
|
+
// GB-03: the registration's own parser decides, before anything is sent (the live registration when this host has it, else the reference registration
|
|
352
|
+
// RustRight published): the command token, the leading global flags, `--if-present` after `run`, the opaque tail of `run` and `exec`, the registered exec
|
|
353
|
+
// targets, the pack destination, and in a snapshot-bound call the snapshot subcommands. No leading option is skipped to find the command, and nothing is rewritten.
|
|
354
|
+
const grammar = (host.pnpmGrammar ?? liveAdapterGrammar({ env, platform, readFile: host.readFile })) ?? PNPM_ADAPTER_GRAMMAR;
|
|
355
|
+
const grammarProblem = adapterArgvRefusal(words, grammar, { snapshot: Boolean(adapterMap?.map) });
|
|
356
|
+
if (grammarProblem) refuse(`pnpm ${words.join(" ")}: ${grammarProblem}; the broker's rightkit-pnpm adapter would refuse it (rightkit#1241), so it is not sent`);
|
|
357
|
+
// `pnpm exec <target> ...`: the door admits only the targets the apps' configs run, and only those the registration admits (`opaqueTargets.exec`).
|
|
358
|
+
const scanned = scanAdapterArgv(words, grammar);
|
|
359
|
+
if (scanned.command === "exec" && !EXEC_TARGETS.includes(scanned.target ?? "")) refuse(`pnpm ${words.join(" ")}: pnpm exec target ${JSON.stringify(scanned.target ?? null)} is not allowed (allowed: ${EXEC_TARGETS.join(", ")}); on a broker-managed host pnpm exec runs only those, as a broker job (rightkit#1241)`);
|
|
360
|
+
return adapterLaunch("rightkit-pnpm", words);
|
|
86
361
|
}
|
|
87
362
|
return refuse(`${program} ${args.join(" ")}: no broker adapter runs this command yet, and on a broker-managed host a build or test command never runs around the broker (rightkit#1241)`);
|
|
88
363
|
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* The production runner of the native compile phases (RA-07, RA-08, RA-09), for the iOS and Android callers to inject: `createNativeRunner({ cwd, env, ... }).run(command, args, options)`.
|
|
367
|
+
* It is the real route and nothing else: `buildLaunch` decides (the registration, the broker's shape and file rules, the executor's client, the typed signing-file handoff), a carried step goes
|
|
368
|
+
* through the carrier in this process (`runCarriedAdapter`: one signed, ledgered request, the receipt verified), an uncarried one through the resolved executor's client (the broker decides). There is NO
|
|
369
|
+
* executor parameter: a test injects a host description (`which`, `exists`, `readFile`, `nativeFs`) to `plan`, which only maps and spawns nothing. A command that is not a native build tool is
|
|
370
|
+
* refused (`RK_NATIVE_TOOL_REFUSED`): a compiler-capable phase never falls back to a generic spawn, and a non-compiling query (`plutil`, `git`) is the caller's own direct step.
|
|
371
|
+
*
|
|
372
|
+
* `plan(command, args, options)` returns the launch (`{ command, args, env?, native }`) or throws the door's refusal (`code` RK_BUILD_LAUNCH_REFUSED, `reason` the typed code).
|
|
373
|
+
* `run(command, args, options)` is synchronous and returns this fixed shape (spawnSync-compatible: `status`, `signal`, `stdout`, `stderr`, `error`):
|
|
374
|
+
* { ok, status, signal, stdout, stderr, error, refusal: { code, message } | null, // a refusal is before any launch: nothing was sent, status is null
|
|
375
|
+
* tool, adapter, capability, argv, carried, relativeCwd, // what was sent (argv after relativization); null on a refusal before the plan
|
|
376
|
+
* requestId, outputDir, // the verified job (carried and verified), else null
|
|
377
|
+
* outputs: { ok, source: "receipt" | "grammar", state, outputs: [{ role, path }], byRole, unresolved?: [role] } | { ok: false, problem } | null,
|
|
378
|
+
* nestedChildren }
|
|
379
|
+
* `outputs` maps the call's managed locations by role under the job's verified output directory: "receipt" when the verified signed receipt's `executable.managedOutputs` was read back and equals the grammar's
|
|
380
|
+
* mapping; "grammar" when the carrier surfaced only the output directory (the result bundle of a test action is then `unresolved` unless the request id is known). A caller consumes `byRole`, never a path of its own.
|
|
381
|
+
* options: `cwd`, `env` (the step's own launch environment, none of it forwarded), `stdio`, `timeout`, `signingFile` and `signerRoot` (the signing-properties file handoff, path only), `brokerCapabilities`.
|
|
382
|
+
*/
|
|
383
|
+
export function createNativeRunner({ cwd = process.cwd(), env = process.env, platform = process.platform, exists = existsSync, which, readFile, nativeFs, signerRoot, brokerCapabilities } = {}) {
|
|
384
|
+
const hostOf = (options) => ({
|
|
385
|
+
env: options.env ?? env, platform, exists, cwd: options.cwd ?? cwd,
|
|
386
|
+
...(which ? { which } : {}), ...(readFile ? { readFile } : {}), ...(nativeFs ? { nativeFs } : {}),
|
|
387
|
+
...(options.signingFile ? { signingFile: options.signingFile } : {}), ...((options.signerRoot ?? signerRoot) ? { signerRoot: options.signerRoot ?? signerRoot } : {}),
|
|
388
|
+
...((options.brokerCapabilities ?? brokerCapabilities) ? { brokerCapabilities: options.brokerCapabilities ?? brokerCapabilities } : {}),
|
|
389
|
+
});
|
|
390
|
+
const plan = (command, args = [], options = {}) => {
|
|
391
|
+
if (nativeToolOf(command) === null) refuse(`RK_NATIVE_TOOL_REFUSED: ${name(command)} ${args.join(" ")}: not a native build tool (xcodegen, xcodebuild, gradle, cmake); a compiler-capable phase has no generic spawn (rightkit#1241), and a non-compiling command is the caller's own direct step`, "RK_NATIVE_TOOL_REFUSED");
|
|
392
|
+
return buildLaunch(command, args.map(String), hostOf(options));
|
|
393
|
+
};
|
|
394
|
+
const refused = (error, tool) => ({
|
|
395
|
+
ok: false, status: null, signal: null, stdout: "", stderr: `${error.message}\n`, error, refusal: { code: error.reason ?? error.code, message: error.message },
|
|
396
|
+
tool: tool ?? null, adapter: tool ? NATIVE_ADAPTERS[tool].adapter : null, capability: tool ? NATIVE_ADAPTERS[tool].capability : null, argv: null, carried: false, relativeCwd: null, requestId: null, outputDir: null, outputs: null, nestedChildren: null,
|
|
397
|
+
});
|
|
398
|
+
const run = (command, args = [], options = {}) => {
|
|
399
|
+
const tool = nativeToolOf(command);
|
|
400
|
+
let launch;
|
|
401
|
+
try { launch = plan(command, args, options); } catch (error) { if (error?.code === BUILD_LAUNCH_REFUSED) return refused(error, tool); throw error; }
|
|
402
|
+
const { native } = launch;
|
|
403
|
+
const runEnv = { ...(options.env ?? env), ...(launch.env ?? {}) };
|
|
404
|
+
const stepCwd = options.cwd ?? cwd;
|
|
405
|
+
let result;
|
|
406
|
+
if (native.carried) {
|
|
407
|
+
const carried = carriedCargoMapOf(runEnv);
|
|
408
|
+
if (!carried?.map) return refused(Object.assign(new Error(`${name(command)}: ${carried?.error ?? "no carried map in the environment"}; a snapshot stage's native step is never sent to the client around its carrier`), { code: BUILD_LAUNCH_REFUSED, reason: "RK_ADAPTER_CARRIER_MISSING" }), tool);
|
|
409
|
+
result = runCarriedAdapter(native.adapter, native.args, { cwd: stepCwd, env: runEnv, stdio: options.stdio === "inherit" ? "inherit" : "capture" }, { map: carried.map });
|
|
410
|
+
} else {
|
|
411
|
+
result = spawnSync(launch.command, launch.args, { cwd: stepCwd, env: runEnv, encoding: "utf8", windowsHide: true, shell: false, maxBuffer: 512 * 1024 * 1024, ...(options.stdio ? { stdio: options.stdio } : {}), ...(options.timeout ? { timeout: options.timeout } : {}) });
|
|
412
|
+
}
|
|
413
|
+
const outputDir = typeof result.outputDir === "string" ? result.outputDir : null;
|
|
414
|
+
const requestId = typeof result.requestId === "string" ? result.requestId : null;
|
|
415
|
+
let outputs = null;
|
|
416
|
+
if (outputDir !== null && result.status === 0) {
|
|
417
|
+
const mapped = { tool: native.tool, args: native.args, outputDir, relativeCwd: native.relativeCwd ?? "", jobId: requestId ?? undefined };
|
|
418
|
+
const managed = result.executable?.managedOutputs;
|
|
419
|
+
if (managed !== undefined) outputs = { ...nativeOutputsFromReceipt(managed, mapped), source: "receipt" };
|
|
420
|
+
else {
|
|
421
|
+
const state = nativeStateDir(outputDir, native.tool, native.relativeCwd ?? "");
|
|
422
|
+
const grammar = nativeManagedOutputs(native.tool, native.args, { state, jobId: requestId ?? undefined });
|
|
423
|
+
if (grammar.problem) outputs = { ok: false, problem: grammar.problem };
|
|
424
|
+
else {
|
|
425
|
+
const unresolved = grammar.outputs.filter((entry) => entry.unresolved).map((entry) => entry.role);
|
|
426
|
+
const resolved = grammar.outputs.filter((entry) => !entry.unresolved).map(({ role, path: file }) => ({ role, path: file }));
|
|
427
|
+
outputs = { ok: true, source: "grammar", state, outputs: resolved, byRole: Object.fromEntries(resolved.map(({ role, path: file }) => [role, file])), ...(unresolved.length ? { unresolved } : {}) };
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
const error = result.error ?? (result.refusal ? Object.assign(new Error(result.refusal.message), { code: result.refusal.code }) : null);
|
|
432
|
+
return {
|
|
433
|
+
ok: result.status === 0 && !error && !result.refusal, status: result.status ?? null, signal: result.signal ?? null, stdout: String(result.stdout ?? ""), stderr: String(result.stderr ?? ""), error,
|
|
434
|
+
refusal: result.refusal ? { code: result.refusal.code, message: result.refusal.message } : null,
|
|
435
|
+
tool: native.tool, adapter: native.adapter, capability: native.capability, argv: [...native.args], carried: native.carried, relativeCwd: native.relativeCwd,
|
|
436
|
+
requestId, outputDir, outputs, nestedChildren: Array.isArray(result.nestedChildren) ? result.nestedChildren : null,
|
|
437
|
+
};
|
|
438
|
+
};
|
|
439
|
+
return Object.freeze({ plan, run });
|
|
440
|
+
}
|