@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/development.mjs
CHANGED
|
@@ -2,7 +2,9 @@ import { discoverBrokerSocket, resolveManagedRoots } from './managed-roots.mjs';
|
|
|
2
2
|
import { spawnSync } from "./background-process.mjs";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, lstatSync, readdirSync, realpathSync, renameSync, rmSync, statSync } from "node:fs";
|
|
5
|
-
import { buildLaunch } from "./build-launch.mjs";
|
|
5
|
+
import { buildLaunch, createNativeRunner } from "./build-launch.mjs";
|
|
6
|
+
import { carriedOutputDir, carriedOutputDirsSince } from "./carried-cargo.mjs";
|
|
7
|
+
import { lockedTauriArgs } from "./tauri-locked.mjs";
|
|
6
8
|
import path from "node:path";
|
|
7
9
|
import { fileURLToPath } from "node:url";
|
|
8
10
|
import { defaultReleaseConfigFile, importReleaseConfig } from "./release-config.mjs";
|
|
@@ -17,8 +19,12 @@ import { stageDevelopmentBinaries, validateStageEntries } from "./stage-binary.m
|
|
|
17
19
|
import { assertStableDesignatedRequirement, readBundleExecutable, requestedCiAdhocSigning, signMacosDevelopmentBundle } from "./development-signing.mjs";
|
|
18
20
|
import { macosCodesignEnvironment } from "./codesign-environment.mjs";
|
|
19
21
|
import { installTauriDevId, resolveTauriDevIdDescriptor, TAURI_DEVID_KIND } from "./install-tauri-devid.mjs";
|
|
22
|
+
import { TAURI_AUTHENTICODE_KIND, installTauriAuthenticode, resolveAuthenticodeDescriptor, validateAuthenticodeBuild } from "./install-tauri-authenticode.mjs";
|
|
23
|
+
import { signWindowsFiles, verifyWindowsFiles } from "./sign-windows.mjs";
|
|
24
|
+
import { assertSuiteSigningPolicy } from "./suite-signing-policy.mjs";
|
|
25
|
+
import { fsListRustFiles } from "./signing-files.mjs";
|
|
20
26
|
import { ASSEMBLED_APP_KIND, installAssembledMac, installAssembledWindows, resolveAssembledInstall } from "./install-assembled-app.mjs";
|
|
21
|
-
import { assertIosDevelopmentSession, installIosDevelopment, iosUiTestCommands, prepareIosDevelopment, prepareIosUiTests, validateIosDevelopment } from "./ios-dev.mjs";
|
|
27
|
+
import { assertIosDevelopmentSession, installIosDevelopment, iosDevelopmentApp, iosUiTestCommands, prepareIosDevelopment, prepareIosUiTests, runIosDevelopmentCommands, validateIosDevelopment } from "./ios-dev.mjs";
|
|
22
28
|
import { packLinuxArtifacts, validateLinuxArchive } from "./linux-artifacts.mjs";
|
|
23
29
|
import { buildAndEmbedNativeHelpers, nativeHelpersFromConfig } from "./native-helpers.mjs";
|
|
24
30
|
import { androidDevelopmentNdkCommand, androidDevelopmentSerial, installAndroidDevelopment, validateAndroidDevelopment } from "./android-dev.mjs";
|
|
@@ -48,9 +54,12 @@ export function developmentEnvironment(env = process.env, platform = process.pla
|
|
|
48
54
|
delete result.CARGO_INCREMENTAL;
|
|
49
55
|
// A dev build is debug-signed: a release signing-properties path never reaches it.
|
|
50
56
|
delete result.RIGHTKIT_ANDROID_SIGNING_PROPERTIES;
|
|
51
|
-
|
|
57
|
+
// A direct (Linux) host never gets the managed agent-bin on PATH or a discovered socket, even when it DECLARES a broker route: managedRuntimeClient then returns the declared,
|
|
58
|
+
// pinned client, but the agent-bin directory and socket here come from environment-derived managed roots that nothing validated. Whatever the build needs from the broker
|
|
59
|
+
// goes through the executor's validated client (buildLaunch / createNativeRunner), never a PATH lookup, and a second endpoint variable would make the declared route ambiguous.
|
|
60
|
+
const client = directPlatform(platform) ? undefined : managedRuntimeClient(result, platform);
|
|
52
61
|
if (client) {
|
|
53
|
-
const { agentBin } = resolveManagedRoots({ env: result });
|
|
62
|
+
const { agentBin } = resolveManagedRoots({ env: result, platform });
|
|
54
63
|
if (agentBin && existsSync(agentBin)) {
|
|
55
64
|
const pathKey = Object.keys(result).find((key) => key.toLowerCase() === "path") ?? "PATH";
|
|
56
65
|
result[pathKey] = [agentBin, result[pathKey]].filter(Boolean).join(path.delimiter);
|
|
@@ -58,7 +67,7 @@ export function developmentEnvironment(env = process.env, platform = process.pla
|
|
|
58
67
|
// Same default as the agent-bin `rightkit` shim: children that run the managed client directly
|
|
59
68
|
// (resolveCommand("rustright"), build scripts) must not lose the broker endpoint.
|
|
60
69
|
const socket = discoverBrokerSocket(result);
|
|
61
|
-
if (!result.RIGHTKIT_BUILD_BROKER_SOCKET && socket && existsSync(socket)) result.
|
|
70
|
+
if (!result.RUSTRIGHT_BUILD_BROKER_SOCKET && !result.RIGHTKIT_BUILD_BROKER_SOCKET && socket && existsSync(socket)) result.RUSTRIGHT_BUILD_BROKER_SOCKET = socket;
|
|
62
71
|
}
|
|
63
72
|
return result;
|
|
64
73
|
}
|
|
@@ -84,6 +93,10 @@ export function validateDevelopmentConfig(config, platform, { root = process.cwd
|
|
|
84
93
|
if (platform !== "mac") throw new Error(`the ${TAURI_DEVID_KIND} development installer is for --platform mac`);
|
|
85
94
|
resolveTauriDevIdDescriptor(target.install, { appRoot: root });
|
|
86
95
|
if (!target.install.targetTriple && !target.install.bundleDirectory) throw new Error(`development.targets.${platform}.install requires targetTriple (or bundleDirectory) to find the built .app`);
|
|
96
|
+
} else if (target.install?.kind === TAURI_AUTHENTICODE_KIND) {
|
|
97
|
+
if (platform !== "win") throw new Error(`the ${TAURI_AUTHENTICODE_KIND} development installer is for --platform win`);
|
|
98
|
+
resolveAuthenticodeDescriptor(target.install, `development.targets.${platform}.install`);
|
|
99
|
+
validateAuthenticodeBuild(target.build, `development.targets.${platform}.build`, (rel) => { try { return readFileSync(path.resolve(root, rel), "utf8"); } catch { return null; } });
|
|
87
100
|
} else if (target.install?.kind === ASSEMBLED_APP_KIND) {
|
|
88
101
|
if (platform !== "mac" && platform !== "win") throw new Error(`the ${ASSEMBLED_APP_KIND} development installer is for --platform mac or win`);
|
|
89
102
|
// The assembly (assembly.<platform>) and the install keys are validated together, before any build.
|
|
@@ -101,13 +114,18 @@ export function validateDevelopmentConfig(config, platform, { root = process.cwd
|
|
|
101
114
|
}
|
|
102
115
|
|
|
103
116
|
export function runDevelopment({ root, platform, config, buildOnly = false, dryRun = false, env = process.env, run = spawnSync, door = buildLaunch,
|
|
104
|
-
codesignEnvironment = macosCodesignEnvironment, serial, uiTests = false }) {
|
|
117
|
+
codesignEnvironment = macosCodesignEnvironment, serial, uiTests = false, hostPlatform = process.platform, nativeHost = process.platform }) {
|
|
105
118
|
// UI test journeys (`right-release test --platform ios`): the declared XCUITest journeys on the dev device, under the same guards.
|
|
106
119
|
if (uiTests && platform !== "ios") throw new Error("UI test journeys run for --platform ios only");
|
|
107
120
|
if (!new Set(["win", "mac", "linux", "android", "ios"]).has(platform)) throw new Error("--platform must be win, mac, linux, android or ios");
|
|
108
121
|
// iOS: --dry-run validates the config and prints the plan on any host; everything else needs macOS with Xcode.
|
|
109
|
-
|
|
122
|
+
// `nativeHost` is the kernel the build runs on (assertNativeDevelopmentHost), `hostPlatform` the host the suite signing policy judges: the same on a real host, apart only in a fixture that
|
|
123
|
+
// runs the Mac or Windows build mechanics on another kernel under a host the signing policy exempts.
|
|
124
|
+
if (!(platform === "ios" && dryRun)) assertNativeDevelopmentHost(platform, nativeHost);
|
|
110
125
|
const target = validateDevelopmentConfig(config, platform, { root });
|
|
126
|
+
// One signing policy, enforced here whatever the config's format (the route probe reads JSON configs only; a legacy .mjs config is judged now): a dev
|
|
127
|
+
// target that would build ad hoc, unsigned or under another identifier is refused before it builds (rightkit#1303). Mac and Dell only.
|
|
128
|
+
if (!dryRun && !uiTests) assertSuiteSigningPolicy({ config, platform: hostPlatform, env, to: "dev", devPlatform: platform, buildOnly, listFiles: fsListRustFiles(root), readFile: (rel) => { try { return readFileSync(path.resolve(root, rel), "utf8"); } catch { return null; } } });
|
|
111
129
|
// Inside the door's source split `root` is the snapshot of a commit (no git directory): the repository is the checkout the door
|
|
112
130
|
// ran in, whose primary-checkout rule and execution policy still apply, and the commit is the split's.
|
|
113
131
|
const split = readSourceSplit({ env, cwd: root });
|
|
@@ -115,7 +133,7 @@ export function runDevelopment({ root, platform, config, buildOnly = false, dryR
|
|
|
115
133
|
assertPrimaryReleaseCheckout(repoRoot, env);
|
|
116
134
|
if (dryRun && uiTests) {
|
|
117
135
|
if (!target.uiTests) throw new Error("development.targets.ios.uiTests is not declared: list the journeys in uiTests.onlyTesting");
|
|
118
|
-
return { built: false, installed: false, plan: { tests: iosUiTestCommands(target, { root,
|
|
136
|
+
return { built: false, installed: false, plan: { tests: iosUiTestCommands(target, { root, destinationId: "<device or simulator id>" }) } };
|
|
119
137
|
}
|
|
120
138
|
if (dryRun) return { built: false, installed: false, plan: { ...(target.ndk ? { ndk: androidDevelopmentNdkCommand(target.ndk) } : {}), build: target.build, install: buildOnly ? null : target.install ?? null, profile: DEVELOPMENT_PROFILE } };
|
|
121
139
|
assertReleaseExecutionAllowed({ cwd: repoRoot, environment: env, command: `right-release dev --platform ${platform}` });
|
|
@@ -138,15 +156,25 @@ export function runDevelopment({ root, platform, config, buildOnly = false, dryR
|
|
|
138
156
|
const startedMs = Date.now();
|
|
139
157
|
// Android: cargo-ndk (Cargo dev profile) fills the generated jniLibs, then the Gradle debug build packages it.
|
|
140
158
|
if (target.ndk) { rmSync(target.ndk.jniLibs, { recursive: true, force: true }); execute(androidDevelopmentNdkCommand(target.ndk), { root, env: childEnv, dryRun, run, door, label: "native build" }); }
|
|
159
|
+
// RA-09: the iOS compile steps (xcodegen, xcodebuild build / build-for-testing / test-without-building) are broker adapter jobs of the production native runner, planned together before the first
|
|
160
|
+
// launch; there is no execute() of this file's own for them. The app's own non-native `prepare` commands are the only direct steps and keep going through the door. The built app and the
|
|
161
|
+
// result bundle are read back from the verified receipt's managed output roles.
|
|
162
|
+
const iosNative = (commands) => runIosDevelopmentCommands(commands, {
|
|
163
|
+
native: createNativeRunner({ cwd: root, env: childEnv }), root, env: childEnv,
|
|
164
|
+
direct: (command) => { execute(command, { root, env: childEnv, dryRun, run, door, label: "prepare" }); return { status: 0 }; },
|
|
165
|
+
});
|
|
141
166
|
if (uiTests) {
|
|
142
|
-
|
|
143
|
-
console.log(`right-release test: ios journeys passed (${target.uiTests.onlyTesting.join(", ")}); result bundle ${
|
|
144
|
-
return { built: true, installed: false, tested: true, resultBundle:
|
|
167
|
+
const tested = iosNative(iosPrepared.tests);
|
|
168
|
+
console.log(`right-release test: ios journeys passed (${target.uiTests.onlyTesting.join(", ")}); result bundle ${tested.resultBundle ?? "(not reported)"}`);
|
|
169
|
+
return { built: true, installed: false, tested: true, resultBundle: tested.resultBundle };
|
|
145
170
|
}
|
|
146
|
-
|
|
171
|
+
let built = null;
|
|
172
|
+
if (iosPrepared) built = iosNative(iosPrepared.build);
|
|
173
|
+
else for (const command of target.build) execute(command, { root, env: childEnv, dryRun, run, door, label: "build" });
|
|
174
|
+
if (iosPrepared?.install && !dryRun) iosPrepared.install.app = iosDevelopmentApp(target, built.outputs);
|
|
147
175
|
// Dev and QA binaries run from one stable staged path, copied in place (no backup of the old copy), never from the
|
|
148
176
|
// broker's per-generation target folder or a hashed dev-targets folder.
|
|
149
|
-
const staged = target.stage?.length ? stageDevelopmentBinaries(target.stage, { root, platform: platform === "win" ? "win32" : process.platform }) : [];
|
|
177
|
+
const staged = target.stage?.length ? stageDevelopmentBinaries(target.stage, { root, platform: platform === "win" ? "win32" : process.platform, managedOutputDirs: carriedOutputDirsSince(childEnv) }) : [];
|
|
150
178
|
for (const file of staged) console.log(`right-release dev: staged ${file}`);
|
|
151
179
|
// Linux: the declared files become one tarball per architecture plus the digest manifest a guest link pins (rightkit#1230).
|
|
152
180
|
// Only from a clean checkout: an artifact others install must name the exact commit it was built from.
|
|
@@ -174,6 +202,22 @@ export function runDevelopment({ root, platform, config, buildOnly = false, dryR
|
|
|
174
202
|
.then((outcome) => ({ built: true, installed: true, ...(staged.length ? { staged } : {}), devid: outcome }))
|
|
175
203
|
.finally(() => lease.release());
|
|
176
204
|
}
|
|
205
|
+
if (target.install.kind === TAURI_AUTHENTICODE_KIND) {
|
|
206
|
+
// The unsigned Windows installer is never run: this kind signs the exe, bundles it, signs the installer, then installs (install-tauri-authenticode.mjs).
|
|
207
|
+
const descriptor = resolveAuthenticodeDescriptor(target.install, `development.targets.${platform}.install`);
|
|
208
|
+
const bundleRoot = resolveBundleRoot(descriptor, { root, env: childEnv, runCargo: runCargoSync });
|
|
209
|
+
const debugDir = path.resolve(bundleRoot, "..", "..");
|
|
210
|
+
const bundleStartedMs = Date.now();
|
|
211
|
+
const authenticode = installTauriAuthenticode({ descriptor, debugDir, appRoot: root, releaseConfig: config, deps: {
|
|
212
|
+
signWindows: (files) => signWindowsFiles(files),
|
|
213
|
+
verifyWindows: (files) => verifyWindowsFiles(files),
|
|
214
|
+
bundle: (command) => execute(command, { root, env: childEnv, dryRun, run, door, label: "bundle" }),
|
|
215
|
+
findInstaller: () => resolveTauriBundleDirectory(descriptor, { root, platform, env: childEnv, sinceMs: bundleStartedMs }).installer,
|
|
216
|
+
install: (installer) => installWindowsDevelopment(descriptor, installer, { root, env: childEnv, run, debugDir }),
|
|
217
|
+
installedExe: () => path.resolve(root, descriptor.expectedInstalledPath),
|
|
218
|
+
} });
|
|
219
|
+
return { built: true, installed: true, ...(staged.length ? { staged } : {}), authenticode };
|
|
220
|
+
}
|
|
177
221
|
if (target.install.kind === ASSEMBLED_APP_KIND) {
|
|
178
222
|
const { spec, descriptor } = resolveAssembledInstall({ config, platform });
|
|
179
223
|
if (platform === "win") {
|
|
@@ -220,7 +264,15 @@ function install(descriptor, { root, platform, config, env, run, door, sinceMs =
|
|
|
220
264
|
if (platform === "win") {
|
|
221
265
|
if (!descriptor.expectedInstalledPath) throw new Error("Windows Tauri installer requires expectedInstalledPath for installed verification");
|
|
222
266
|
const { bundleRoot, installer: app } = resolveTauriBundleDirectory(descriptor, { root, platform, env, sinceMs });
|
|
223
|
-
|
|
267
|
+
return installWindowsDevelopment(descriptor, app, { root, env, run, debugDir: path.resolve(bundleRoot, "..", "..") });
|
|
268
|
+
}
|
|
269
|
+
return installMacosDevelopment(descriptor, { root, platform, config, env, run });
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Run the silent NSIS installer of a Windows dev build in place and check the installed exe is this build's. */
|
|
273
|
+
function installWindowsDevelopment(descriptor, app, { root, env, run, debugDir }) {
|
|
274
|
+
{
|
|
275
|
+
const built = path.resolve(debugDir, path.basename(descriptor.expectedInstalledPath));
|
|
224
276
|
assertNonEmptyExecutable(built);
|
|
225
277
|
// Tauri's silent installer force-kills a running copy of the app (utils.nsh: IfSilent kill). When it
|
|
226
278
|
// was running, pass /R so the installer relaunches it as the user (RunAsUser, outside this process tree).
|
|
@@ -234,8 +286,10 @@ function install(descriptor, { root, platform, config, env, run, door, sinceMs =
|
|
|
234
286
|
if (result.status !== 0) throw new Error(`development installer failed (${result.status}): ${app}`);
|
|
235
287
|
const installed = path.resolve(root, descriptor.expectedInstalledPath);
|
|
236
288
|
assertInstalledExecutableMatches(built, installed);
|
|
237
|
-
return;
|
|
238
289
|
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function installMacosDevelopment(descriptor, { root, platform, config, env, run }) {
|
|
239
293
|
const app = resolveTauriArtifact(descriptor, { root, platform, env });
|
|
240
294
|
// One fixed install path per app: macOS keys privacy grants to it. No /Applications/<name> guess.
|
|
241
295
|
if (!descriptor.destination) throw new DevelopmentInstallError("DEV_INSTALL_DESTINATION_REQUIRED", "development.targets.mac.install.destination is required: the one fixed path this app installs to (for example /Applications/<Product>.app)");
|
|
@@ -301,11 +355,36 @@ export function resolveTauriArtifact(descriptor, { root, platform, env = process
|
|
|
301
355
|
return resolveTauriBundleDirectory(descriptor, { root, platform, env, runCargo }).installer;
|
|
302
356
|
}
|
|
303
357
|
|
|
358
|
+
/**
|
|
359
|
+
* GB-60 (GATE output pinning; E16/E19): a build whose compile is receipt-managed (a broker host's carried build) writes its outputs under the broker's
|
|
360
|
+
* output directory a signed receipt named, never under the source tree or the snapshot. A configured path that reads "target/..." (relative to the Cargo
|
|
361
|
+
* workspace) is therefore migrated EXPLICITLY onto that directory; a source-relative output path that is not under a `target` directory cannot hold what that
|
|
362
|
+
* build wrote and is refused with a typed error. It is never silently ignored, never resolved to a stale same-name file in the snapshot, and never
|
|
363
|
+
* replaced by a scan for the newest match. `declared` is the configured path relative to `root`; `manifestDir` the Cargo manifest's directory relative to it.
|
|
364
|
+
*/
|
|
365
|
+
export function receiptRelativeOutputPath({ declared, root, manifestDir = ".", managedOutputDir, label }) {
|
|
366
|
+
const relative = path.relative(path.resolve(root), path.resolve(root, declared)).split(path.sep).join("/");
|
|
367
|
+
const segments = relative.split("/").filter(Boolean);
|
|
368
|
+
const manifestParts = String(manifestDir).split(/[\\/]/).filter((part) => part && part !== ".");
|
|
369
|
+
const at = segments.findIndex((segment, index) => segment === "target" && segments.slice(0, index).every((part, i) => part === manifestParts[i]));
|
|
370
|
+
if (at < 0) {
|
|
371
|
+
const error = new Error(`${label} ${JSON.stringify(declared)} is a source-relative path, but this build is receipt-managed: its outputs are under ${managedOutputDir}, not the source tree or the snapshot. Declare it relative to the Cargo target directory (target/<triple>/debug/bundle/...), or omit it so the receipt's directory is used`);
|
|
372
|
+
error.code = "DEV_OUTPUT_SOURCE_RELATIVE";
|
|
373
|
+
throw error;
|
|
374
|
+
}
|
|
375
|
+
return path.join(managedOutputDir, ...segments.slice(at + 1));
|
|
376
|
+
}
|
|
377
|
+
|
|
304
378
|
export function resolveTauriBundleDirectory(descriptor, { root, platform, env = process.env, runCargo = runCargoSync, sinceMs = null }) {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
379
|
+
let bundleRoot;
|
|
380
|
+
if (descriptor.bundleDirectory) {
|
|
381
|
+
const declared = path.resolve(root, descriptor.bundleDirectory);
|
|
382
|
+
if (!inside(root, declared)) throw new Error("Tauri bundleDirectory must stay inside app root");
|
|
383
|
+
const managed = descriptor.manifestPath ? carriedOutputDir(env, path.resolve(root, descriptor.manifestPath), { preferNested: true }) : null;
|
|
384
|
+
bundleRoot = managed
|
|
385
|
+
? receiptRelativeOutputPath({ declared: descriptor.bundleDirectory, root, manifestDir: path.dirname(descriptor.manifestPath), managedOutputDir: managed, label: "Tauri bundleDirectory" })
|
|
386
|
+
: declared;
|
|
387
|
+
} else bundleRoot = resolveBundleRoot(descriptor, { root, env, runCargo });
|
|
309
388
|
const entries = readdirSync(bundleRoot, { withFileTypes: true });
|
|
310
389
|
const candidates = platform === "mac"
|
|
311
390
|
? entries.filter((entry) => entry.isDirectory() && entry.name === `${descriptor.productName}.app`).map((entry) => path.join(bundleRoot, entry.name))
|
|
@@ -345,6 +424,10 @@ function assertNonEmptyExecutable(file) { if (!existsSync(file) || !statSync(fil
|
|
|
345
424
|
function resolveBundleRoot(descriptor, { root, env, runCargo }) {
|
|
346
425
|
if (!descriptor.manifestPath) throw new Error("Tauri installer requires manifestPath when bundleDirectory is omitted");
|
|
347
426
|
const manifest = path.resolve(root, descriptor.manifestPath);
|
|
427
|
+
// The bundle root is under the output directory a signed receipt of this stage named, not under a read-only metadata call
|
|
428
|
+
// (the compile the Tauri CLI made nested under the build step's carried adapter job, from the broker's signed list of its children).
|
|
429
|
+
const fromReceipt = carriedOutputDir(env, manifest, { preferNested: true });
|
|
430
|
+
if (fromReceipt) return path.join(fromReceipt, descriptor.targetTriple, cargoProfileDirectory(DEVELOPMENT_PROFILE), "bundle", platformBundleKind(descriptor));
|
|
348
431
|
const cwd = process.cwd();
|
|
349
432
|
let metadata;
|
|
350
433
|
try {
|
|
@@ -353,7 +436,9 @@ function resolveBundleRoot(descriptor, { root, env, runCargo }) {
|
|
|
353
436
|
} finally { process.chdir(cwd); }
|
|
354
437
|
if (metadata.error || metadata.status !== 0) throw new Error(`RightKit Cargo metadata failed (${metadata.status ?? "unavailable"})${metadata.error ? `: ${metadata.error.message}` : ""}`);
|
|
355
438
|
let targetDirectory;
|
|
356
|
-
|
|
439
|
+
// A carried call (a snapshot stage's, carried-cargo.mjs) reports the signed receipt's output directory, not what metadata prints.
|
|
440
|
+
if (typeof metadata.outputDir === "string") targetDirectory = metadata.outputDir;
|
|
441
|
+
else try { targetDirectory = JSON.parse(metadata.stdout).target_directory; } catch { throw new Error("RightKit Cargo metadata returned invalid JSON"); }
|
|
357
442
|
if (!targetDirectory || !path.isAbsolute(targetDirectory)) throw new Error("Cargo metadata omitted absolute target_directory");
|
|
358
443
|
return path.join(targetDirectory, descriptor.targetTriple, cargoProfileDirectory(DEVELOPMENT_PROFILE), "bundle", platformBundleKind(descriptor));
|
|
359
444
|
}
|
|
@@ -364,7 +449,7 @@ function execute(command, { root, env, dryRun, run, label, door = null }) {
|
|
|
364
449
|
validateCommand(command, `development ${label}`);
|
|
365
450
|
const cwd = path.resolve(root, command.cwd ?? ".");
|
|
366
451
|
if (!inside(root, cwd)) throw new Error(`development ${label} cwd must stay inside app root`);
|
|
367
|
-
const args = (command.args ?? []).map(String);
|
|
452
|
+
const args = lockedTauriArgs((command.args ?? []).map(String), env);
|
|
368
453
|
if (dryRun) return { cmd: command.cmd, args, cwd };
|
|
369
454
|
// A pnpm step through the door is the broker's rightkit-pnpm job (or plain pnpm on a direct host), launched by name: the door never uses
|
|
370
455
|
// the node entry resolveCommand would find, so resolving pnpm's JavaScript CLI here would only fail a host that does not need it.
|
|
@@ -375,8 +460,8 @@ function execute(command, { root, env, dryRun, run, label, door = null }) {
|
|
|
375
460
|
// is the rightkit-pnpm broker job (by its own name, not the node entry resolveCommand finds), anything else as resolved.
|
|
376
461
|
// A configured command can run code regardless of its label, so installation commands use the door too.
|
|
377
462
|
const own = resolved.command === process.execPath && resolved.args[0] === OWN_RIGHT_RELEASE;
|
|
378
|
-
const launch = own || !door ? resolved : { ...resolved, ...door(command.cmd === "pnpm" ? "pnpm" : resolved.command, command.cmd === "pnpm" ? args : resolved.args, { env: resolved.env ?? env, cwd, exists: existsSync }) };
|
|
379
|
-
const result = run(launch.command, launch.args, { cwd, env: launch.env ?? env, stdio: "inherit", windowsHide: true, shell: false, timeout: command.timeoutMs });
|
|
463
|
+
const launch = own || !door ? resolved : { ...resolved, ...door(command.cmd === "pnpm" ? "pnpm" : resolved.command, command.cmd === "pnpm" ? args : resolved.args, { env: resolved.env ?? env, stepEnv: command.env, cwd, exists: existsSync }) };
|
|
464
|
+
const result = run(launch.command, launch.args, { cwd, env: launch.env && launch.env !== resolved.env ? { ...(resolved.env ?? env), ...launch.env } : (launch.env ?? env), stdio: "inherit", windowsHide: true, shell: false, timeout: command.timeoutMs });
|
|
380
465
|
if (result.status !== 0) throw new Error(`development ${label} command failed (${result.status}): ${command.name ?? command.cmd}`);
|
|
381
466
|
}
|
|
382
467
|
/** This package's own right-release entry: the door's RightKit, whatever the app's node_modules holds. */
|
|
@@ -403,7 +488,7 @@ export function resolveCommand(command, args, { root, env = process.env, platfor
|
|
|
403
488
|
const client = managedRuntimeClient(env);
|
|
404
489
|
if (!client) throw new Error("installed RightKit runtime client is unavailable for development command");
|
|
405
490
|
const socket = discoverBrokerSocket(env);
|
|
406
|
-
const childEnv = !env.RIGHTKIT_BUILD_BROKER_SOCKET && existsSync(socket) ? { ...env,
|
|
491
|
+
const childEnv = !env.RUSTRIGHT_BUILD_BROKER_SOCKET && !env.RIGHTKIT_BUILD_BROKER_SOCKET && existsSync(socket) ? { ...env, RUSTRIGHT_BUILD_BROKER_SOCKET: socket } : undefined;
|
|
407
492
|
return { ...managedRuntimeCommand(client, args), ...(childEnv ? { env: childEnv } : {}) };
|
|
408
493
|
}
|
|
409
494
|
return { command, args };
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// GB-29 (GATE-CONTRACT GC-L01; R-L, A-L, P-L; E3, E4, E5): direct Linux origin trust, with no broker, no new service, no custom signature scheme and no fabricated broker receipt.
|
|
2
|
+
//
|
|
3
|
+
// A Linux origin runs the gate direct. Its compile and package stages leave DIRECT-ORIGIN records: the gate's own evidence (commit, the snapshot tree at start and at exit, the
|
|
4
|
+
// command identity, the output digest), never presented as, and never carrying the fields of, a broker's signed receipt. The record is NOT authority: it is a claim a verifier
|
|
5
|
+
// re-derives. The hosted side recomputes, from the dispatched commit alone, everything it can (the real git tree, the unit's version, the unit's content digest, its own full gate) and
|
|
6
|
+
// admits only a record whose every bound field equals ITS value; what it cannot recompute (that a local compile ran) is recorded as the origin's claim and grants nothing. The
|
|
7
|
+
// candidate identity (repository, commit, tree, unit, version, registry and the exact bytes) is what dispatch and publication join or reject on, so a replay of another candidate, a
|
|
8
|
+
// different artifact under one identity, a broker-shaped or non-Linux record, a stale tree or a digest the hosted pack does not reproduce are each refused by name.
|
|
9
|
+
//
|
|
10
|
+
// Activation is NOT here: widening the publish workflow's `admission` input beyond `broker` is the reserved publication authorization (GC-T22), so this verifier is the model and its tests,
|
|
11
|
+
// and the qualification a direct Linux origin writes (unit-gate.mjs) is verified by it locally exactly as the hosted side will, before anything could be dispatched.
|
|
12
|
+
import { createHash } from "node:crypto";
|
|
13
|
+
|
|
14
|
+
export const DIRECT_ORIGIN_SCHEMA = "rightkit.direct-origin/1";
|
|
15
|
+
/** The stages a direct origin evidences (the attested stages of a broker host). */
|
|
16
|
+
export const DIRECT_ORIGIN_STAGES = Object.freeze(["compile", "package"]);
|
|
17
|
+
export const DIRECT_ORIGIN_REFUSED = "RK_DIRECT_ORIGIN_REFUSED";
|
|
18
|
+
/** The repository whose units a direct origin releases. */
|
|
19
|
+
export const RIGHTKIT_REPOSITORY = "bogusyogi/rightkit";
|
|
20
|
+
/** Fields only a broker's signed job receipt or a broker request carries: a direct record that has any is a fabricated broker receipt and is refused. */
|
|
21
|
+
export const BROKER_ONLY_FIELDS = Object.freeze(["verdict", "refreshed", "reusedFrom", "provenance", "signature", "payload", "jobId", "requestId", "brokerVersion", "keyId", "nonce"]);
|
|
22
|
+
const RECORD_KEYS = ["schema", "executor", "repository", "commit", "tree", "unit", "registry", "artifact", "stages", "candidate"];
|
|
23
|
+
const STAGE_KEYS = ["stage", "ok", "commit", "tree", "treeAtStart", "treeAtExit", "command", "outputSha256"];
|
|
24
|
+
const SHA256 = /^[0-9a-f]{64}$/;
|
|
25
|
+
const SHA1 = /^[0-9a-f]{40}$/;
|
|
26
|
+
|
|
27
|
+
const sha256 = (text) => createHash("sha256").update(text).digest("hex");
|
|
28
|
+
const canonical = (value) => (Array.isArray(value) ? `[${value.map(canonical).join(",")}]` : value !== null && typeof value === "object" ? `{${Object.keys(value).sort().map((k) => `${JSON.stringify(k)}:${canonical(value[k])}`).join(",")}}` : JSON.stringify(value));
|
|
29
|
+
const refuse = (message) => { throw Object.assign(new Error(`${DIRECT_ORIGIN_REFUSED}: ${message}`), { code: DIRECT_ORIGIN_REFUSED }); };
|
|
30
|
+
const same = (a, b) => canonical(a) === canonical(b);
|
|
31
|
+
const isObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
32
|
+
|
|
33
|
+
/** The identity dispatch and publication join or reject on: what a candidate IS, independent of who built it or when. */
|
|
34
|
+
export function candidateIdentity({ repository, commit, tree, unit, registry, artifact }) {
|
|
35
|
+
return sha256(canonical({ repository, commit, tree, unit: { kind: unit.kind, name: unit.name, version: unit.version }, registry, artifactSha256: artifact.artifactSha256, contentSha256: artifact.contentSha256 ?? null }));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** One stage's evidence on a direct origin: the gate's own facts, taken after the stage guard confirmed the snapshot was the commit's bytes at start and at exit. */
|
|
39
|
+
export function directStageEvidence({ stage, commit, tree, treeAtStart = tree, treeAtExit = tree, command, outputSha256 = null }) {
|
|
40
|
+
return { stage, ok: true, commit, tree, treeAtStart, treeAtExit, command, outputSha256 };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The record a direct Linux origin writes into its qualification (`directOrigin`). `stages` is `{ compile, package }` of directStageEvidence. */
|
|
44
|
+
export function buildDirectOrigin({ repository, commit, tree, unit, registry, platform, artifact, stages }) {
|
|
45
|
+
const record = { schema: DIRECT_ORIGIN_SCHEMA, executor: { kind: "direct", platform }, repository, commit, tree, unit: { kind: unit.kind, name: unit.name, version: unit.version }, registry, artifact: { artifactSha256: artifact.artifactSha256, contentSha256: artifact.contentSha256 ?? null }, stages: DIRECT_ORIGIN_STAGES.map((stage) => stages[stage]) };
|
|
46
|
+
return { ...record, candidate: candidateIdentity({ repository, commit, tree, unit, registry, artifact: record.artifact }) };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function noBrokerFields(value, where) {
|
|
50
|
+
if (Array.isArray(value)) { value.forEach((item, i) => noBrokerFields(item, `${where}[${i}]`)); return; }
|
|
51
|
+
if (!isObject(value)) return;
|
|
52
|
+
for (const [key, inner] of Object.entries(value)) {
|
|
53
|
+
if (BROKER_ONLY_FIELDS.includes(key)) refuse(`${where}.${key} is a broker receipt field: a direct origin has no broker and a direct record never carries or imitates one`);
|
|
54
|
+
noBrokerFields(inner, `${where}.${key}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The verifier (hosted side, and the origin's own self-check). `expected` is what the verifier derives independently:
|
|
60
|
+
* `{ repository, commit, tree (the real git tree of the commit), unit: { kind, name, version }, registry, contentSha256 (the unit's content digest recomputed from the commit; required for an
|
|
61
|
+
* npm unit), artifactSha256 (a crate's archive digest, when recomputed) }`. Throws RK_DIRECT_ORIGIN_REFUSED naming the first thing that is not so; returns `{ candidate }`.
|
|
62
|
+
*/
|
|
63
|
+
export function verifyDirectOrigin(record, expected) {
|
|
64
|
+
if (!isObject(record)) refuse("the direct-origin record is not an object");
|
|
65
|
+
noBrokerFields(record, "directOrigin");
|
|
66
|
+
if (!same(Object.keys(record).sort(), [...RECORD_KEYS].sort())) refuse(`the record has unexpected or missing keys (${Object.keys(record).sort().join(", ")})`);
|
|
67
|
+
if (record.schema !== DIRECT_ORIGIN_SCHEMA) refuse(`schema ${JSON.stringify(record.schema)} is not ${DIRECT_ORIGIN_SCHEMA}`);
|
|
68
|
+
// Mac and the Dell always run through their broker; a direct record is a Linux origin's and nothing else's.
|
|
69
|
+
if (!isObject(record.executor) || !same(Object.keys(record.executor).sort(), ["kind", "platform"]) || record.executor.kind !== "direct" || record.executor.platform !== "linux") refuse(`the executor ${JSON.stringify(record.executor)} is not a direct Linux origin (Mac and Dell builds are broker-attested, never direct)`);
|
|
70
|
+
if (record.repository !== expected.repository) refuse(`the record is for repository ${record.repository}, not ${expected.repository}`);
|
|
71
|
+
if (!SHA1.test(record.commit ?? "") || record.commit !== expected.commit) refuse(`the record is for commit ${String(record.commit).slice(0, 12)}, not ${String(expected.commit).slice(0, 12)}`);
|
|
72
|
+
if (!SHA1.test(record.tree ?? "") || record.tree !== expected.tree) refuse(`the record's tree ${String(record.tree).slice(0, 12)} is not the commit's real tree ${String(expected.tree).slice(0, 12)}`);
|
|
73
|
+
if (!isObject(record.unit) || record.unit.kind !== expected.unit.kind || record.unit.name !== expected.unit.name || record.unit.version !== expected.unit.version) refuse(`the record covers ${JSON.stringify(record.unit)}, not ${JSON.stringify(expected.unit)}`);
|
|
74
|
+
if (record.registry !== expected.registry) refuse(`the record is for registry ${record.registry}, not ${expected.registry}`);
|
|
75
|
+
const artifact = record.artifact;
|
|
76
|
+
if (!isObject(artifact) || !SHA256.test(artifact.artifactSha256 ?? "")) refuse("the record names no artifact sha256");
|
|
77
|
+
if (expected.unit.kind === "npm") {
|
|
78
|
+
if (!SHA256.test(expected.contentSha256 ?? "")) refuse("the verifier has no recomputed content digest for this npm unit");
|
|
79
|
+
if (artifact.contentSha256 !== expected.contentSha256) refuse(`the record's content sha256 ${artifact.contentSha256} is not the ${expected.contentSha256} the verifier recomputed from the commit`);
|
|
80
|
+
} else if (expected.artifactSha256 !== undefined && artifact.artifactSha256 !== expected.artifactSha256) refuse(`the record's artifact sha256 ${artifact.artifactSha256} is not the ${expected.artifactSha256} the verifier recomputed`);
|
|
81
|
+
if (!Array.isArray(record.stages) || record.stages.length !== DIRECT_ORIGIN_STAGES.length) refuse(`the record carries ${Array.isArray(record.stages) ? record.stages.length : "no"} stage records, not ${DIRECT_ORIGIN_STAGES.join(" and ")}`);
|
|
82
|
+
record.stages.forEach((stage, i) => {
|
|
83
|
+
const name = DIRECT_ORIGIN_STAGES[i];
|
|
84
|
+
if (!isObject(stage) || !same(Object.keys(stage).sort(), [...STAGE_KEYS].sort())) refuse(`the ${name} stage record has unexpected or missing keys`);
|
|
85
|
+
if (stage.stage !== name || stage.ok !== true) refuse(`stage ${i + 1} is ${JSON.stringify(stage.stage)} ok=${stage.ok}, not a passed ${name}`);
|
|
86
|
+
if (stage.commit !== expected.commit || stage.tree !== expected.tree) refuse(`the ${name} stage is for another commit or tree`);
|
|
87
|
+
// The snapshot was the commit's bytes when the stage started and when it ended: a stale or mutated tree is no evidence.
|
|
88
|
+
if (stage.treeAtStart !== expected.tree || stage.treeAtExit !== expected.tree) refuse(`the ${name} stage's snapshot tree changed (start ${String(stage.treeAtStart).slice(0, 12)}, exit ${String(stage.treeAtExit).slice(0, 12)}, commit ${expected.tree.slice(0, 12)})`);
|
|
89
|
+
if (typeof stage.command !== "string" || !stage.command) refuse(`the ${name} stage names no command`);
|
|
90
|
+
if (name === "package" && stage.outputSha256 !== artifact.artifactSha256) refuse("the package stage's output is not the record's artifact");
|
|
91
|
+
});
|
|
92
|
+
const candidate = candidateIdentity({ repository: expected.repository, commit: expected.commit, tree: expected.tree, unit: expected.unit, registry: expected.registry, artifact });
|
|
93
|
+
if (record.candidate !== candidate) refuse("the record's candidate identity is not the one its own fields give (a replayed or edited record)");
|
|
94
|
+
return { candidate };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Dispatch and publication join or reject on the candidate identity (GC-T21): the same identity and bytes JOIN the existing claim; a different candidate or different bytes for the same
|
|
99
|
+
* dispatch key (repository, commit, unit, version, registry) REJECT; no claim is created here. `existing` is the claim already held for the key, or null.
|
|
100
|
+
*/
|
|
101
|
+
export function reconcileClaim(existing, record) {
|
|
102
|
+
const key = sha256(canonical({ repository: record.repository, commit: record.commit, unit: record.unit, registry: record.registry }));
|
|
103
|
+
if (existing === null || existing === undefined) return { action: "claim", key, candidate: record.candidate };
|
|
104
|
+
if (existing.key !== key) refuse("the existing claim is for another dispatch key");
|
|
105
|
+
if (existing.candidate === record.candidate && existing.artifactSha256 === record.artifact.artifactSha256) return { action: "join", key, candidate: record.candidate };
|
|
106
|
+
refuse(`a claim for this dispatch key already binds candidate ${String(existing.candidate).slice(0, 12)} (artifact ${String(existing.artifactSha256).slice(0, 12)}); conflicting bytes are rejected, never replaced`);
|
|
107
|
+
}
|
package/disk-command.mjs
CHANGED
|
@@ -822,6 +822,7 @@ export async function runDiskCommand(args = process.argv.slice(2), { env = proce
|
|
|
822
822
|
if (!purged) {
|
|
823
823
|
purged = purgeHoldings({ policy, platform, env, ...deps.purge });
|
|
824
824
|
stdout(`purged ${purged.results.filter((r) => r.ok).length} holding dir(s), ${formatBytes(purged.purgedBytes)}`);
|
|
825
|
+
for (const r of purged.results.filter((x) => !x.ok)) stdout(`purge FAILED: ${r.path}: ${r.error}`);
|
|
825
826
|
}
|
|
826
827
|
const after = check();
|
|
827
828
|
const warn = after.warn;
|
package/disk-policy.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { closeSync, cpSync, existsSync, fstatSync, lstatSync, mkdirSync, openSync, opendirSync, readFileSync, readSync, readdirSync, realpathSync, renameSync, rmSync, statSync, statfsSync, appendFileSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { chmodSync, closeSync, cpSync, existsSync, fstatSync, lstatSync, mkdirSync, openSync, opendirSync, readFileSync, readSync, readdirSync, realpathSync, renameSync, rmSync, statSync, statfsSync, appendFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { spawnSync } from "./background-process.mjs";
|
|
6
|
+
import { brokerClientInvocation } from "./broker-client.mjs";
|
|
6
7
|
|
|
7
8
|
export const DISK_EXIT_WARNING = 3;
|
|
8
9
|
export const DEFAULT_POLICY_FILE = path.join(path.dirname(fileURLToPath(import.meta.url)), "disk-policy.json");
|
|
@@ -339,7 +340,8 @@ export const BUDGET_PARTS = ["targets", "sccache", "cargoHome", "toolchains", "t
|
|
|
339
340
|
* missing, fails, times out or answers with anything else. Nothing is cached and no shell is used. */
|
|
340
341
|
export function readRustrightBudget({ run = spawnSync } = {}) {
|
|
341
342
|
try {
|
|
342
|
-
const
|
|
343
|
+
const launch = brokerClientInvocation("rustright", ["doctor", "--json", "--no-scan"], { encoding: "utf8", timeout: 20_000, shell: false, maxBuffer: 1 << 24 }, { probe: run });
|
|
344
|
+
const r = run(launch.command, launch.args, launch.options);
|
|
343
345
|
if (!r || r.error || r.status !== 0) return null;
|
|
344
346
|
const budget = JSON.parse(String(r.stdout)).buildBudget;
|
|
345
347
|
if (!budget || typeof budget !== "object") return null;
|
|
@@ -1029,7 +1031,20 @@ function newestWriteMs(dir) {
|
|
|
1029
1031
|
* moved into or written in the dir within the last `days` either, so every item is held at least `days` even though the
|
|
1030
1032
|
* folder name carries only a date. Only dirs this tool created: the base must carry the holding marker (or, for the legacy
|
|
1031
1033
|
* heavy dir, the tool's own log), the child must be a real directory named YYYY-MM-DD, never a symlink. */
|
|
1032
|
-
|
|
1034
|
+
/** Remove a held tree whose items arrived read-only (frozen sources, Go module caches): make every real directory owner-writable
|
|
1035
|
+
* first, never following symlinks, since `rmSync` cannot unlink entries inside a directory it may not write. */
|
|
1036
|
+
function removeHeld(dir) {
|
|
1037
|
+
const walk = (d) => {
|
|
1038
|
+
try { chmodSync(d, lstatSync(d).mode | 0o700); } catch { return; }
|
|
1039
|
+
let names = [];
|
|
1040
|
+
try { names = readdirSync(d); } catch { return; }
|
|
1041
|
+
for (const n of names) { const c = path.join(d, n); try { if (lstatSync(c).isDirectory()) walk(c); } catch { /* vanished */ } }
|
|
1042
|
+
};
|
|
1043
|
+
walk(dir);
|
|
1044
|
+
rmSync(dir, { recursive: true, force: true });
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
export function purgeHoldings({ policy, platform = process.platform, env = process.env, home = os.homedir(), now = Date.now(), days = policy.holdingDays ?? 3, dryRun = false, bases, remove = removeHeld, size = walkSize } = {}) {
|
|
1033
1048
|
const key = platformKey(platform);
|
|
1034
1049
|
const legacy = policy.heavyVolume?.[key];
|
|
1035
1050
|
const list = bases ?? [...knownHoldingBases({ policy, platform, env, home }), ...(legacy ? [legacy] : [])];
|