@rightkit/release 0.6.8 → 0.6.11
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 +30 -12
- package/admission-files.mjs +4 -1
- package/admission.mjs +2 -2
- package/advisory-exceptions.mjs +41 -5
- package/android-play-upload.mjs +16 -15
- package/app-check.mjs +105 -27
- package/assemble-app-command.mjs +151 -0
- package/assembled-app.mjs +445 -0
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/darwin/rightkit-swap.sign.json +6 -0
- package/bin/rightkit-gate.mjs +1608 -345
- package/broker-pins.mjs +152 -0
- package/build-launch.mjs +88 -0
- package/build-ownership.mjs +1231 -0
- package/build-release.mjs +82 -28
- package/cargo-contract.mjs +3 -1
- package/cargo-ledger.mjs +62 -0
- package/cargo-target-shim.mjs +238 -0
- package/class-a-launch-guard.mjs +126 -0
- package/cli/right-release.mjs +31 -5
- package/clippy-equivalence.mjs +202 -0
- package/clippy-route.mjs +543 -0
- package/codesign-shim.mjs +11 -1
- package/committed-source.mjs +39 -5
- package/crate-workspaces.mjs +87 -0
- package/create-mac-updater.mjs +14 -2
- package/deterministic-scans.mjs +997 -71
- package/development-signing.mjs +4 -1
- package/development.mjs +120 -21
- package/direct-receipt.mjs +135 -0
- package/door-policy.mjs +71 -6
- package/door.mjs +6 -6
- package/freshness-census.mjs +257 -0
- package/gate-executor.mjs +14 -2
- package/gate-stages.mjs +91 -15
- package/heavy-command.mjs +3 -2
- package/hook-retirement.mjs +65 -0
- package/init-command.mjs +1 -1
- package/install-app.mjs +11 -2
- package/install-assembled-app.mjs +423 -0
- package/inventory-check.mjs +11 -8
- package/ios-dev.mjs +70 -3
- package/ios-upload.mjs +20 -15
- package/linux-artifacts.mjs +93 -0
- package/local-publish.mjs +224 -53
- package/local-qualify.mjs +5 -3
- package/local-runtime.mjs +5 -4
- package/mac-package-recipe.mjs +4 -1
- package/managed-cargo.mjs +47 -5
- package/managed-root-guard.mjs +34 -0
- package/mirror-root-artifact.mjs +18 -5
- package/native-helpers.mjs +355 -0
- package/ownership-class-rules.mjs +1 -1
- package/ownership-evidence.mjs +37 -13
- package/ownership-hooks.mjs +20 -59
- package/ownership-lockfiles.mjs +14 -1
- package/ownership-policy-bundle.mjs +3 -2
- package/ownership-staged.mjs +2 -1
- package/ownership.mjs +24 -0
- package/package-recipe-targets.mjs +84 -10
- package/package.json +4 -3
- package/pipeline-admission.mjs +4 -4
- package/pipeline-receipts.mjs +302 -302
- package/preflight.mjs +27 -4
- package/project-type.mjs +2 -2
- package/provenance-verify.mjs +115 -0
- package/publish-cargo.mjs +12 -9
- package/qa-admission-policy.json +10 -1
- package/recipe-build.mjs +106 -14
- package/release-artifact-path.mjs +341 -0
- package/release-config.mjs +1 -1
- package/release-invocation.mjs +8 -1
- package/release-qa-command.mjs +25 -9
- package/release-source-brand.mjs +17 -0
- package/release-source-identity.mjs +171 -0
- package/release.mjs +59 -22
- package/rightkit-versions.json +73 -47
- package/runtime-budgets.mjs +6 -2
- package/scan-digest.mjs +71 -5
- package/sign-updater.mjs +6 -3
- package/snapshot-broker.mjs +172 -0
- package/snapshot-check.mjs +312 -0
- package/snapshot-git.mjs +166 -0
- package/snapshot-request.mjs +363 -0
- package/snapshot-root-config.mjs +76 -0
- package/snapshot.mjs +969 -0
- package/source-split.mjs +73 -0
- package/stage-commands.mjs +60 -0
- package/stage-ledger.mjs +204 -0
- package/stage-sidecar-command.mjs +5 -1
- package/stage-sidecar.mjs +157 -27
- package/swap-helper-bundle.mjs +5 -2
- package/test-command.mjs +17 -4
- package/unit-gate.mjs +301 -0
- package/web-build.mjs +40 -6
- package/win-package-recipe.mjs +20 -7
package/stage-sidecar.mjs
CHANGED
|
@@ -38,7 +38,7 @@ import os from "node:os";
|
|
|
38
38
|
import path from "node:path";
|
|
39
39
|
import { spawnSync as nodeSpawnSync } from "./background-process.mjs";
|
|
40
40
|
import { resolveCommand } from "./development.mjs";
|
|
41
|
-
import { managedHost } from "./managed-cargo.mjs";
|
|
41
|
+
import { directPlatform, managedHost } from "./managed-cargo.mjs";
|
|
42
42
|
import { APPROVAL_MACHO_STRIP_LINKEDIT_V1, APPROVAL_RAW_SHA256, approvalDigest } from "./macho-approval-hash.mjs";
|
|
43
43
|
import {
|
|
44
44
|
bundledFileName, planMediaSidecar, sourceOfferText, stagedFileName, validateMediaSidecarConfig, verifyStagedSidecars, writeMediaSidecarNotices,
|
|
@@ -111,7 +111,7 @@ const KEYS = Object.freeze({
|
|
|
111
111
|
"native-library": ["kind", "library", "version", "catalog", "resourceDir", "licenseDir", "cacheDir", "as", "files", "companions", "source", "platforms"],
|
|
112
112
|
resource: ["kind", "from", "to", "sha256", "machO", "clean", "platforms"],
|
|
113
113
|
command: ["kind", "name", "cmd", "args", "cwd", "output", "externalBin", "dest", "clean", "platforms"],
|
|
114
|
-
remote: ["kind", "name", "url", "sha256", "dest", "to", "size", "extract", "memberSha256", "executable", "clean", "platforms"],
|
|
114
|
+
remote: ["kind", "name", "url", "sha256", "dest", "to", "size", "extract", "extractKind", "memberSha256", "executable", "clean", "platforms"],
|
|
115
115
|
cmake: ["kind", "name", "repo", "commit", "target", "output", "cmakeArgs", "platformArgs", "generator", "platformGenerators", "architecture", "cmakeBin", "smoke", "sha256", "dest", "clean", "platforms"],
|
|
116
116
|
prebuilt: ["kind", "name", "from", "manifest", "dest", "platforms"],
|
|
117
117
|
});
|
|
@@ -476,10 +476,15 @@ export function validateSidecarManifest(raw, { target, mode = "release" } = {})
|
|
|
476
476
|
if (item.size !== undefined && (!Number.isSafeInteger(item.size) || item.size <= 0)) errors.push(`${where}: size must be a positive integer (bytes)`);
|
|
477
477
|
if (item.extract !== undefined) { const member = normalizeArchiveMember(item.extract); if (member.error || (!member.byPath && !NAME.test(member.value))) errors.push(`${where}: extract must be the bare file name of one archive member or its archive path (posix, no leading /, no .. segment): ${member.error ?? "invalid file name"}`); }
|
|
478
478
|
if (item.memberSha256 !== undefined && (item.extract === undefined || !SHA256.test(item.memberSha256 ?? ""))) errors.push(`${where}: memberSha256 must be a lowercase 64-hex SHA-256 and needs extract`);
|
|
479
|
+
if (item.extractKind !== undefined && item.extractKind !== "nsis") errors.push(`${where}: extractKind must be "nsis" (omit it for tar-readable zip/tgz/nupkg archives)`);
|
|
480
|
+
if (item.extractKind === "nsis") {
|
|
481
|
+
if (item.extract === undefined) errors.push(`${where}: extractKind "nsis" needs extract (the installer member path, e.g. bin/smartctl.exe)`);
|
|
482
|
+
if (typeof item.memberSha256 !== "string" || !SHA256.test(item.memberSha256)) errors.push(`${where}: extractKind "nsis" requires memberSha256 (the extracted member's lowercase 64-hex SHA-256); an NSIS member is never staged unpinned`);
|
|
483
|
+
}
|
|
479
484
|
if (item.executable !== undefined && typeof item.executable !== "boolean") errors.push(`${where}: executable must be a boolean`);
|
|
480
485
|
const clean = cleanOf();
|
|
481
486
|
if (dest && !skipped) claim(dest, where);
|
|
482
|
-
Object.assign(entry, { name, url: item.url, sha256: item.sha256, dest, ...(item.size ? { size: item.size } : {}), ...(item.extract ? { extract: item.extract } : {}), ...(item.memberSha256 ? { memberSha256: item.memberSha256 } : {}), ...(item.executable ? { executable: true } : {}), ...(clean ? { clean } : {}) });
|
|
487
|
+
Object.assign(entry, { name, url: item.url, sha256: item.sha256, dest, ...(item.extractKind ? { extractKind: item.extractKind } : {}), ...(item.size ? { size: item.size } : {}), ...(item.extract ? { extract: item.extract } : {}), ...(item.memberSha256 ? { memberSha256: item.memberSha256 } : {}), ...(item.executable ? { executable: true } : {}), ...(clean ? { clean } : {}) });
|
|
483
488
|
} else if (item.kind === "cmake") {
|
|
484
489
|
const name = item.name;
|
|
485
490
|
if (typeof name !== "string" || !NAME.test(name) || /\.exe$/i.test(name) || name !== path.basename(name)) errors.push(`${where}: name must be a bare sidecar name (no path, no .exe, no target triple)`);
|
|
@@ -611,6 +616,45 @@ export function defaultExtractMember({ archive, fileName, destDir, run = default
|
|
|
611
616
|
return out;
|
|
612
617
|
}
|
|
613
618
|
|
|
619
|
+
/** 7-Zip candidates: an explicit path (deps.sevenZip or RIGHTKIT_SEVEN_ZIP), then `7z` on PATH, then 7z.exe under Program Files on win32. */
|
|
620
|
+
export function sevenZipCandidates({ explicit, env = process.env, platform = process.platform } = {}) {
|
|
621
|
+
const list = [];
|
|
622
|
+
if (explicit) list.push(explicit);
|
|
623
|
+
else if (env.RIGHTKIT_SEVEN_ZIP) list.push(env.RIGHTKIT_SEVEN_ZIP);
|
|
624
|
+
list.push("7z");
|
|
625
|
+
if (platform === "win32") for (const base of [env.ProgramFiles ?? "C:\\Program Files", env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)"]) list.push(path.win32.join(base, "7-Zip", "7z.exe"));
|
|
626
|
+
return [...new Set(list)];
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Extract one member of an NSIS installer with 7-Zip (`x -y -o<dir> <installer> <member>`; nothing in the installer is run), then
|
|
631
|
+
* require its SHA-256 to equal the pinned one. Fails closed, naming the installer digest, member, expected and actual digests, on a
|
|
632
|
+
* missing tool, a failed extraction, a missing member or any mismatch. Returns the extracted file path under destDir.
|
|
633
|
+
*/
|
|
634
|
+
export function defaultExtractNsisMember({ archive, fileName, destDir, expectedSha256, installerSha256, run = defaultRun, sevenZip, env = process.env, platform = process.platform, readFile = nodeFs.readFileSync, exists = nodeFs.existsSync }) {
|
|
635
|
+
const wanted = normalizeArchiveMember(fileName);
|
|
636
|
+
const who = `NSIS installer ${installerSha256 ?? archive} member ${JSON.stringify(fileName)}`;
|
|
637
|
+
if (wanted.error) fail("STAGE_SIDECAR_EXTRACT_FAILED", `${who} ${wanted.error}`);
|
|
638
|
+
if (!SHA256.test(expectedSha256 ?? "")) fail("STAGE_SIDECAR_EXTRACT_FAILED", `${who}: a pinned member sha256 is required`);
|
|
639
|
+
let tool;
|
|
640
|
+
let lastError = "not found";
|
|
641
|
+
for (const candidate of sevenZipCandidates({ explicit: sevenZip, env, platform })) {
|
|
642
|
+
const result = run(candidate, ["x", "-y", `-o${destDir}`, archive, wanted.value], { cwd: path.dirname(archive) });
|
|
643
|
+
if (result.error || result.status === null || result.status === undefined) { lastError = result.error?.message ?? "could not start"; continue; }
|
|
644
|
+
tool = candidate;
|
|
645
|
+
if (result.status !== 0) fail("STAGE_SIDECAR_EXTRACT_FAILED", `${who}: ${candidate} exited ${result.status}; nothing staged`);
|
|
646
|
+
break;
|
|
647
|
+
}
|
|
648
|
+
if (!tool) fail("STAGE_SIDECAR_EXTRACT_FAILED", `${who}: 7-Zip is required and was not found (${lastError}); install 7-Zip or set RIGHTKIT_SEVEN_ZIP; nothing staged`);
|
|
649
|
+
const out = path.join(destDir, ...wanted.value.split("/"));
|
|
650
|
+
const inside = path.relative(path.resolve(destDir), path.resolve(out));
|
|
651
|
+
if (!inside || inside === ".." || inside.startsWith(`..${path.sep}`) || path.isAbsolute(inside)) fail("STAGE_SIDECAR_EXTRACT_FAILED", `${who} resolves outside the extraction directory`);
|
|
652
|
+
if (!exists(out)) fail("STAGE_SIDECAR_EXTRACT_FAILED", `${who}: member not found after extraction; nothing staged`);
|
|
653
|
+
const actual = sha256Of(readFile(out));
|
|
654
|
+
if (actual !== expectedSha256) fail("STAGE_SIDECAR_HASH_MISMATCH", `${who}: sha256 is ${actual}, expected ${expectedSha256}; nothing staged`, { expected: expectedSha256, actual });
|
|
655
|
+
return out;
|
|
656
|
+
}
|
|
657
|
+
|
|
614
658
|
async function defaultFetchBytes(url) {
|
|
615
659
|
if (!url.startsWith("https://")) fail("STAGE_SIDECAR_CONFIG_INVALID", `refusing a non-https archive URL: ${url}`);
|
|
616
660
|
const response = await fetch(url);
|
|
@@ -621,8 +665,8 @@ async function defaultFetchBytes(url) {
|
|
|
621
665
|
/** Absolute catalog file for a validated spec. Crate resolution reads the rightkit metadata listing; npm walks up node_modules. */
|
|
622
666
|
function resolveCatalogFile(spec, ctx, kind) {
|
|
623
667
|
const label = kind === "ffmpeg" ? "rightkit-ffmpeg" : "native-libraries";
|
|
624
|
-
if (spec.via === "absolute") return spec.file;
|
|
625
|
-
if (spec.via === "path") return path.resolve(ctx.appRoot, spec.file);
|
|
668
|
+
if (spec.via === "absolute") return requireSnapshotSource(ctx, spec.file, `the ${label} catalog`);
|
|
669
|
+
if (spec.via === "path") return requireSnapshotSource(ctx, path.resolve(ctx.appRoot, spec.file), `the ${label} catalog`);
|
|
626
670
|
if (spec.via === "crate") {
|
|
627
671
|
const meta = ctx.run("rustright", ["cargo", "metadata", "--format-version", "1"], { cwd: ctx.appRoot, env: ctx.env });
|
|
628
672
|
let packages;
|
|
@@ -707,6 +751,23 @@ function cleanDest(ctx, entry, dest, { directory = false } = {}) {
|
|
|
707
751
|
try { ctx.fs.rmSync(dest, { recursive: directory, force: true }); } catch (error) { fail("STAGE_SIDECAR_CLEAN_REFUSED", `cannot clean ${dest} (${error.message}); a directory is only removed for a directory resource`, { dest, cause: error }); }
|
|
708
752
|
}
|
|
709
753
|
|
|
754
|
+
/**
|
|
755
|
+
* Warm-cache rule (GATE-CONTRACT "rebuild only what changed"): a staged file carries the mtime of the cache file it was copied from, a
|
|
756
|
+
* lipo or merge output the newest mtime of its inputs, so a re-stage after the snapshot sync (which removes every ledger entry) does not
|
|
757
|
+
* bump the mtime and re-trigger the app crate (tauri-build emits rerun-if-changed for the staged sidecar). Bytes are bound by the
|
|
758
|
+
* ledger, never by mtime. A file system without `utimesSync` or `statSync().mtime` (a test double) is left as written.
|
|
759
|
+
*/
|
|
760
|
+
function carryMtime(ctx, dest, sources) {
|
|
761
|
+
const { fs } = ctx;
|
|
762
|
+
if (typeof fs.utimesSync !== "function") return;
|
|
763
|
+
let newest = null;
|
|
764
|
+
for (const source of sources) {
|
|
765
|
+
let st; try { st = fs.statSync(source); } catch { continue; }
|
|
766
|
+
if (st?.mtime instanceof Date && (!newest || st.mtime > newest)) newest = st.mtime;
|
|
767
|
+
}
|
|
768
|
+
if (newest) { try { fs.utimesSync(dest, newest, newest); } catch { /* the mtime is a cache hint, never a binding */ } }
|
|
769
|
+
}
|
|
770
|
+
|
|
710
771
|
function copyInto(ctx, source, dest, { executable }) {
|
|
711
772
|
const { fs, windows } = ctx;
|
|
712
773
|
const sourceSha256 = sha256Of(fs.readFileSync(source));
|
|
@@ -721,11 +782,44 @@ function copyInto(ctx, source, dest, { executable }) {
|
|
|
721
782
|
} else if (executable && !windows) fs.chmodSync(dest, 0o755);
|
|
722
783
|
const sha256 = sha256Of(fs.readFileSync(dest));
|
|
723
784
|
if (sha256 !== sourceSha256) fail("STAGE_SIDECAR_HASH_MISMATCH", `staged ${dest} differs from its source ${source}`, { expected: sourceSha256, actual: sha256 });
|
|
785
|
+
carryMtime(ctx, dest, [source]);
|
|
724
786
|
return { sha256, sizeBytes: fs.statSync(dest).size, changed };
|
|
725
787
|
}
|
|
726
788
|
|
|
727
789
|
const rel = (ctx, file) => path.relative(ctx.appRoot, file).replaceAll("\\", "/");
|
|
728
790
|
|
|
791
|
+
/**
|
|
792
|
+
* The boundary of a split run (row 7.05a): the only checkout bytes it may read are cache entries whose digest the commit pins or
|
|
793
|
+
* tool-invalidated build caches, both under `ctx.cacheRoot`. A SOURCE the config names (a local `source.dir`, an ffmpeg `path`) must
|
|
794
|
+
* resolve inside the snapshot's app root, never into the checkout or anywhere else.
|
|
795
|
+
*/
|
|
796
|
+
function requireSnapshotSource(ctx, file, label) {
|
|
797
|
+
if (!ctx.split) return file;
|
|
798
|
+
const relative = path.relative(ctx.appRoot, path.resolve(file));
|
|
799
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) fail("STAGE_SIDECAR_SOURCE_OUTSIDE_SNAPSHOT", `${label} resolves to ${file}, outside the snapshot's app root ${ctx.appRoot}; a split run reads sources only from the snapshot (checkout bytes enter only as digest-pinned cache entries)`, { file });
|
|
800
|
+
return file;
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* The environment a split run hands to the commands it starts (a `command` entry, an ffmpeg `stage` command): Cargo's output directory is
|
|
804
|
+
* the checkout's, resolved there once and passed absolute, so a command that reaches Cargo (`rustright cargo run -p ...`) never writes
|
|
805
|
+
* `target/` into the snapshot. Without a split, or when the checkout holds no Cargo workspace, the environment is returned as it is.
|
|
806
|
+
*/
|
|
807
|
+
function splitCommandEnv(ctx) {
|
|
808
|
+
if (!ctx.split) return ctx.env;
|
|
809
|
+
if (ctx.splitEnv === undefined) {
|
|
810
|
+
const resolved = ctx.run("rustright", ["cargo", "metadata", "--format-version", "1", "--no-deps"], { cwd: ctx.cacheRoot, env: ctx.env });
|
|
811
|
+
let dir;
|
|
812
|
+
try { dir = JSON.parse(resolved?.stdout).target_directory; } catch { /* no workspace in the checkout: nothing to pin */ }
|
|
813
|
+
ctx.splitEnv = !resolved?.error && resolved?.status === 0 && typeof dir === "string" && path.isAbsolute(dir) ? { ...ctx.env, CARGO_TARGET_DIR: path.resolve(dir) } : ctx.env;
|
|
814
|
+
}
|
|
815
|
+
return ctx.splitEnv;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/** A cache directory the config names (`cacheDir`): the checkout's `.right-release` under a split, the app root otherwise. */
|
|
819
|
+
const cacheDirFor = (ctx, dir) => (ctx.split ? path.resolve(ctx.cacheRoot, ".right-release", dir) : path.resolve(ctx.appRoot, dir));
|
|
820
|
+
/** The per-arch slice directory of a universal staging: absolute under the checkout's cache root on a split, app-relative otherwise. */
|
|
821
|
+
const sliceDirFor = (ctx, triple) => (ctx.split ? path.resolve(ctx.cacheRoot, SLICE_ROOT, triple) : `${SLICE_ROOT}/${triple}`);
|
|
822
|
+
|
|
729
823
|
/** `lipo -archs <file>` as a list; a failing lipo is a typed error. */
|
|
730
824
|
function lipoArchs(ctx, file) {
|
|
731
825
|
const result = ctx.run("lipo", ["-archs", file], { cwd: ctx.appRoot, env: ctx.env });
|
|
@@ -760,6 +854,7 @@ function lipoMergeInto(ctx, slices, dest, label, { executable = false } = {}) {
|
|
|
760
854
|
fs.rmSync(temporary, { force: true });
|
|
761
855
|
if (executable) fs.chmodSync(dest, 0o755);
|
|
762
856
|
}
|
|
857
|
+
carryMtime(ctx, dest, slices);
|
|
763
858
|
return { sha256, sizeBytes: fs.statSync(dest).size, changed };
|
|
764
859
|
}
|
|
765
860
|
|
|
@@ -790,6 +885,7 @@ function stageCargoBin(entry, ctx) {
|
|
|
790
885
|
const changed = !(fs.existsSync(dest) && sha256Of(fs.readFileSync(dest)) === sha256);
|
|
791
886
|
fs.chmodSync(temporary, 0o755);
|
|
792
887
|
fs.renameSync(temporary, dest);
|
|
888
|
+
carryMtime(ctx, dest, slices.map((slice) => slice.source));
|
|
793
889
|
return [{ kind: "cargo-bin", name: entry.name, path: rel(ctx, dest), sha256, sizeBytes: fs.statSync(dest).size, changed, sourceSha256: sha256, package: entry.package, bin: entry.bin, profile: entry.profile, features: entry.features, externalBin: !entry.dest, slices: slices.map((slice) => ({ triple: slice.triple, sha256: slice.sourceSha256 })) }];
|
|
794
890
|
}
|
|
795
891
|
|
|
@@ -807,17 +903,29 @@ function stageCargoBuild(entry, ctx, triple) {
|
|
|
807
903
|
"-p", entry.package, "--bin", entry.bin, "--target", triple, "--profile", entry.profile,
|
|
808
904
|
...(entry.features.length ? ["--features", entry.features.join(",")] : []), "--message-format=json-render-diagnostics"];
|
|
809
905
|
ctx.log(`stage-sidecar: rightkit ${args.join(" ")}`);
|
|
810
|
-
|
|
906
|
+
// From a snapshot, Cargo would resolve `target/` (or a relative build.target-dir) against the snapshot: its output directory is
|
|
907
|
+
// resolved in the checkout instead and passed as an absolute CARGO_TARGET_DIR, so the warm outputs stay the checkout's and the
|
|
908
|
+
// snapshot holds no target/ (docs/GATE-CONTRACT.md "Output directory & cache").
|
|
909
|
+
let buildEnv = env;
|
|
910
|
+
if (ctx.split) {
|
|
911
|
+
const checkoutManifest = entry.manifestPath ? ["--manifest-path", path.resolve(ctx.cacheRoot, entry.manifestPath)] : [];
|
|
912
|
+
const resolved = run("rustright", ["cargo", "metadata", "--format-version", "1", "--no-deps", ...checkoutManifest], { cwd: ctx.cacheRoot, env });
|
|
913
|
+
let pinned;
|
|
914
|
+
try { pinned = JSON.parse(resolved?.stdout).target_directory; } catch { /* refused below */ }
|
|
915
|
+
if (resolved?.error || resolved?.status !== 0 || typeof pinned !== "string" || !path.isAbsolute(pinned)) fail("STAGE_SIDECAR_BUILD_FAILED", `the checkout's cargo output directory for ${entry.package} cannot be resolved (${resolved?.error?.message ?? `exit ${resolved?.status}`}); nothing was built`, { package: entry.package });
|
|
916
|
+
buildEnv = { ...env, CARGO_TARGET_DIR: path.resolve(pinned) };
|
|
917
|
+
}
|
|
918
|
+
const built = run("rustright", args, { cwd: appRoot, env: buildEnv });
|
|
811
919
|
if (built?.error || built?.status !== 0) fail("STAGE_SIDECAR_BUILD_FAILED", `rustright cargo build of ${entry.package} failed (${built?.error?.message ?? `exit ${built?.status}`})`, { package: entry.package, stderr: tail(built?.stderr) });
|
|
812
920
|
let source = artifactFromStream(built.stdout, entry.bin);
|
|
813
921
|
if (!source || !fs.existsSync(source)) {
|
|
814
|
-
const meta = run("rustright", ["cargo", "metadata", "--format-version", "1", "--no-deps", ...(entry.manifestPath ? ["--manifest-path", path.resolve(appRoot, entry.manifestPath)] : [])], { cwd: appRoot, env });
|
|
922
|
+
const meta = run("rustright", ["cargo", "metadata", "--format-version", "1", "--no-deps", ...(entry.manifestPath ? ["--manifest-path", path.resolve(appRoot, entry.manifestPath)] : [])], { cwd: appRoot, env: buildEnv });
|
|
815
923
|
let targetDir;
|
|
816
924
|
try { targetDir = JSON.parse(meta.stdout).target_directory; } catch { /* refused below */ }
|
|
817
925
|
if (meta?.status !== 0 || typeof targetDir !== "string" || !path.isAbsolute(targetDir)) targetDir = undefined;
|
|
818
926
|
// A managed target root: the broker/cargo metadata target_directory (which cargo derives from CARGO_TARGET_DIR) is
|
|
819
927
|
// authoritative; an explicit CARGO_TARGET_DIR in this environment is tried too, resolved against the app root.
|
|
820
|
-
const envDir = typeof
|
|
928
|
+
const envDir = typeof buildEnv.CARGO_TARGET_DIR === "string" && buildEnv.CARGO_TARGET_DIR.trim() ? path.resolve(appRoot, buildEnv.CARGO_TARGET_DIR) : undefined;
|
|
821
929
|
if (!targetDir && !envDir) fail("STAGE_SIDECAR_BINARY_NOT_FOUND", `cargo reported no ${entry.bin} executable and its target directory cannot be read`, { bin: entry.bin });
|
|
822
930
|
const candidates = builtBinaryCandidates({ targetDir, targetDirs: envDir ? [envDir] : [], triple, profile: entry.profile, bin: entry.bin, windows: ctx.windows });
|
|
823
931
|
source = candidates.find((file) => fs.existsSync(file));
|
|
@@ -833,7 +941,7 @@ const ffmpegSourceFor = (sources, tool, arch) => { const spec = sources?.[tool];
|
|
|
833
941
|
/** The binary a local ffmpeg source names: the file itself, or `<dir>/<member>` / `<dir>/bin/<member>` (`.exe` added for a Windows target). */
|
|
834
942
|
function resolveLocalFfmpegSource(ctx, spec, tool) {
|
|
835
943
|
const { fs } = ctx;
|
|
836
|
-
const base = path.resolve(ctx.appRoot, spec.path);
|
|
944
|
+
const base = requireSnapshotSource(ctx, path.resolve(ctx.appRoot, spec.path), `the ffmpeg ${tool} source path`);
|
|
837
945
|
if (fs.existsSync(base) && fs.statSync(base).isFile()) return base;
|
|
838
946
|
const member = spec.member ?? `${tool}${ctx.windows ? ".exe" : ""}`;
|
|
839
947
|
const candidates = [path.join(base, member), path.join(base, "bin", member)];
|
|
@@ -911,15 +1019,17 @@ function probeFfmpegBinary(ctx, { file, tool, target }) {
|
|
|
911
1019
|
*/
|
|
912
1020
|
async function stageFfmpegSlice(entry, ctx, resolved, { config, t, writeNotices, flat = false }) {
|
|
913
1021
|
const { appRoot, run, env, fs } = ctx;
|
|
914
|
-
|
|
1022
|
+
// A universal slice is staged under SLICE_ROOT, which a split keeps in the checkout's cache root, never in the snapshot.
|
|
1023
|
+
const sliceRoot = ctx.split && String(config.stageDir ?? "").startsWith(SLICE_ROOT) ? ctx.cacheRoot : appRoot;
|
|
1024
|
+
const planned = planMediaSidecar({ config, catalog: resolved.catalog, crateDir: resolved.crateDir, crateVersion: resolved.crateVersion, catalogBytes: resolved.catalogBytes, root: sliceRoot });
|
|
915
1025
|
const plan = flat ? { ...planned, files: planned.files.map((file) => ({ ...file, stagedPath: path.resolve(appRoot, entry.resourceDir, file.bundledName) })) } : planned;
|
|
916
1026
|
await fetchFfmpegSources(entry, ctx, plan, t.arch);
|
|
917
1027
|
if (config.stage) {
|
|
918
1028
|
const universal = ctx.target.arch === "universal";
|
|
919
1029
|
// A universal entry stages once per slice: `{arch}`, `{triple}` and `{stageDir}` in the stage command's arguments name the slice.
|
|
920
|
-
const args = (config.stage.args ?? []).map((arg) => (universal ? String(arg).replaceAll("{arch}", t.arch).replaceAll("{triple}", t.triple).replaceAll("{stageDir}", config.stageDir) : arg));
|
|
1030
|
+
const args = (config.stage.args ?? []).map((arg) => (universal ? String(arg).replaceAll("{arch}", t.arch).replaceAll("{triple}", t.triple).replaceAll("{stageDir}", sliceRoot === appRoot ? config.stageDir : path.resolve(sliceRoot, config.stageDir)) : arg));
|
|
921
1031
|
ctx.log(`stage-sidecar: ${config.stage.cmd} ${args.join(" ")}`);
|
|
922
|
-
const staged = run(config.stage.cmd, args, { cwd: appRoot, env });
|
|
1032
|
+
const staged = run(config.stage.cmd, args, { cwd: appRoot, env: splitCommandEnv(ctx) });
|
|
923
1033
|
if (staged?.error || staged?.status !== 0) fail("STAGE_SIDECAR_BUILD_FAILED", `ffmpeg stage command failed (${staged?.error?.message ?? `exit ${staged?.status}`})`);
|
|
924
1034
|
}
|
|
925
1035
|
const noticeFiles = writeNotices ? writeMediaSidecarNotices(plan, { exists: fs.existsSync, mkdir: fs.mkdirSync, copy: fs.copyFileSync, write: fs.writeFileSync }) : [];
|
|
@@ -943,13 +1053,20 @@ async function stageFfmpegUniversal(entry, ctx, resolved) {
|
|
|
943
1053
|
results.push({ slice, ...(await stageFfmpegSlice(entry, ctx, resolved, { config: slice.media, t: parseTargetTriple(slice.triple), writeNotices: false })) });
|
|
944
1054
|
}
|
|
945
1055
|
// One notice set and one source offer that names the rows of every slice (a per-slice offer would overwrite the other).
|
|
1056
|
+
// A split plans each slice under the checkout's cache root (SLICE_ROOT), so the slice plans name their notices there too. The notices are FINAL
|
|
1057
|
+
// outputs and belong in the snapshot beside the merged file: they are rebased onto the app root before anything is written, never into the checkout.
|
|
1058
|
+
const toApp = (file) => {
|
|
1059
|
+
if (!ctx.split) return file;
|
|
1060
|
+
const relative = path.relative(ctx.cacheRoot, file);
|
|
1061
|
+
return relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative) ? file : path.resolve(appRoot, relative);
|
|
1062
|
+
};
|
|
946
1063
|
const noticeByDest = new Map();
|
|
947
|
-
for (const { plan } of results) for (const notice of plan.notices) noticeByDest.set(notice.dest, notice);
|
|
1064
|
+
for (const { plan } of results) for (const notice of plan.notices) noticeByDest.set(toApp(notice.dest), { ...notice, dest: toApp(notice.dest) });
|
|
948
1065
|
const rowsById = new Map();
|
|
949
1066
|
for (const { plan } of results) for (const row of plan.rows) rowsById.set(row.id, row);
|
|
950
1067
|
const combined = {
|
|
951
1068
|
notices: [...noticeByDest.values()],
|
|
952
|
-
offerPath: results[0].plan.offerPath,
|
|
1069
|
+
offerPath: toApp(results[0].plan.offerPath),
|
|
953
1070
|
offer: sourceOfferText({ rows: [...rowsById.values()], contact: entry.media.notices.sourceOfferContact, crateVersion: resolved.crateVersion }),
|
|
954
1071
|
};
|
|
955
1072
|
const noticeFiles = writeMediaSidecarNotices(combined, { exists: fs.existsSync, mkdir: fs.mkdirSync, copy: fs.copyFileSync, write: fs.writeFileSync });
|
|
@@ -1185,14 +1302,14 @@ async function prepareNativeFile(entry, ctx, loaded, item, { destDir, scratchDir
|
|
|
1185
1302
|
if (fs.existsSync(dest) && !nativeDigestMismatch(entry, row, fs.readFileSync(dest), { fromSource: Boolean(entry.source) })) return prepared;
|
|
1186
1303
|
|
|
1187
1304
|
if (entry.source) {
|
|
1188
|
-
const file = path.resolve(appRoot, entry.source.dir, row.artifactFileName);
|
|
1305
|
+
const file = requireSnapshotSource(ctx, path.resolve(appRoot, entry.source.dir, row.artifactFileName), `source.dir of the ${where} row ${row.id}`);
|
|
1189
1306
|
if (!(fs.existsSync(file) && fs.statSync(file).isFile())) fail("STAGE_SIDECAR_BINARY_NOT_FOUND", `the ${where} row ${row.id} is staged from source.dir ${entry.source.dir}, which has no ${row.artifactFileName} (looked at ${file}); nothing is downloaded for a source dir`, { file });
|
|
1190
1307
|
const mismatch = nativeDigestMismatch(entry, row, fs.readFileSync(file), { fromSource: true });
|
|
1191
1308
|
if (mismatch) failNativeMismatch(row, file, mismatch);
|
|
1192
1309
|
return { ...prepared, from: file };
|
|
1193
1310
|
}
|
|
1194
1311
|
if (!row.archive) fail("STAGE_SIDECAR_CONFIG_INVALID", `the ${where} row ${row.id} has no archive, so it is staged from a local build: set source: { dir } on this entry to the directory holding ${row.artifactFileName}`);
|
|
1195
|
-
const archive = path.
|
|
1312
|
+
const archive = path.join(cacheDirFor(ctx, entry.cacheDir), archiveFileName(row.archive.url));
|
|
1196
1313
|
if (!(fs.existsSync(archive) && sha256Of(fs.readFileSync(archive)) === row.archive.sha256)) {
|
|
1197
1314
|
const bytes = await ctx.fetchBytes(row.archive.url);
|
|
1198
1315
|
const actual = sha256Of(bytes);
|
|
@@ -1334,7 +1451,7 @@ async function stageNativeLibrary(entry, ctx) {
|
|
|
1334
1451
|
}
|
|
1335
1452
|
// Otherwise each arch's files are staged thin under SLICE_ROOT, then merged file by file into the entry's resource directory.
|
|
1336
1453
|
const slices = [];
|
|
1337
|
-
for (const plan of plans) slices.push({ triple: plan.triple, files: await stageNativeSet(entry, ctx, loaded, plan.items, { destDir:
|
|
1454
|
+
for (const plan of plans) slices.push({ triple: plan.triple, files: await stageNativeSet(entry, ctx, loaded, plan.items, { destDir: sliceDirFor(ctx, plan.triple) }) });
|
|
1338
1455
|
const [first, second] = slices.map((slice) => slice.files.map((file) => file.outName));
|
|
1339
1456
|
if ([...first].sort().join("\n") !== [...second].sort().join("\n")) fail("STAGE_SIDECAR_NOT_APPROVED", `the arch rows name different library files (aarch64: ${first.join(", ")}; x86_64: ${second.join(", ")}); each file needs one name on both arches (a single file can be renamed with "as")`);
|
|
1340
1457
|
return first.map((name) => {
|
|
@@ -1402,7 +1519,7 @@ function stageCommand(entry, ctx) {
|
|
|
1402
1519
|
const cwd = entry.cwd ? path.resolve(appRoot, entry.cwd) : appRoot;
|
|
1403
1520
|
cleanDest(ctx, entry, destFor(entry, ctx));
|
|
1404
1521
|
ctx.log(`stage-sidecar: ${entry.cmd} ${entry.args.join(" ")}`);
|
|
1405
|
-
const built = run(entry.cmd, entry.args, { cwd, env });
|
|
1522
|
+
const built = run(entry.cmd, entry.args, { cwd, env: splitCommandEnv(ctx) });
|
|
1406
1523
|
if (built?.error || built?.status !== 0) fail("STAGE_SIDECAR_BUILD_FAILED", `command ${entry.name} (${entry.cmd}) failed (${built?.error?.message ?? `exit ${built?.status}`})`, { name: entry.name, stderr: tail(built?.stderr) });
|
|
1407
1524
|
const source = path.resolve(appRoot, entry.output);
|
|
1408
1525
|
if (!fs.existsSync(source) || !fs.statSync(source).isFile()) fail("STAGE_SIDECAR_BINARY_NOT_FOUND", `command ${entry.name} did not produce ${entry.output}`, { name: entry.name, output: entry.output });
|
|
@@ -1419,8 +1536,8 @@ function stageCommand(entry, ctx) {
|
|
|
1419
1536
|
* fails closed with nothing staged. Returns { cached, fromCache }.
|
|
1420
1537
|
*/
|
|
1421
1538
|
async function ensureCachedDownload(ctx, { url, sha256, size }) {
|
|
1422
|
-
const {
|
|
1423
|
-
const cached = path.resolve(
|
|
1539
|
+
const { fs } = ctx;
|
|
1540
|
+
const cached = path.resolve(ctx.cacheRoot, REMOTE_CACHE_DIR, sha256);
|
|
1424
1541
|
if (fs.existsSync(cached) && sha256Of(fs.readFileSync(cached)) === sha256) return { cached, fromCache: true };
|
|
1425
1542
|
ctx.log(`stage-sidecar: GET ${url}`);
|
|
1426
1543
|
let response;
|
|
@@ -1453,7 +1570,9 @@ async function stageRemote(entry, ctx) {
|
|
|
1453
1570
|
let memberSha256;
|
|
1454
1571
|
if (entry.extract) {
|
|
1455
1572
|
scratch = ctx.makeTempDir();
|
|
1456
|
-
source =
|
|
1573
|
+
source = entry.extractKind === "nsis"
|
|
1574
|
+
? await ctx.extractNsisMember({ archive: cached, fileName: entry.extract, destDir: scratch, expectedSha256: entry.memberSha256, installerSha256: entry.sha256 })
|
|
1575
|
+
: await ctx.extractMember({ archive: cached, fileName: entry.extract, destDir: scratch });
|
|
1457
1576
|
memberSha256 = sha256Of(fs.readFileSync(source));
|
|
1458
1577
|
if (entry.memberSha256 && memberSha256 !== entry.memberSha256) fail("STAGE_SIDECAR_HASH_MISMATCH", `${entry.extract} from ${entry.url} is ${memberSha256}, expected ${entry.memberSha256}; nothing staged`, { expected: entry.memberSha256, actual: memberSha256 });
|
|
1459
1578
|
}
|
|
@@ -1462,7 +1581,7 @@ async function stageRemote(entry, ctx) {
|
|
|
1462
1581
|
const copied = copyInto(ctx, source, dest, { executable: entry.executable === true });
|
|
1463
1582
|
const expected = entry.extract ? entry.memberSha256 : entry.sha256;
|
|
1464
1583
|
if (expected && copied.sha256 !== expected) fail("STAGE_SIDECAR_HASH_MISMATCH", `staged ${entry.dest} is ${copied.sha256}, expected ${expected}`, { expected, actual: copied.sha256 });
|
|
1465
|
-
return [{ kind: "remote", name: entry.name, path: rel(ctx, dest), ...copied, sourceSha256: memberSha256 ?? entry.sha256, url: entry.url, cached: fromCache, externalBin: false, ...(entry.extract ? { archiveSha256: entry.sha256, member: entry.extract } : {}) }];
|
|
1584
|
+
return [{ kind: "remote", name: entry.name, path: rel(ctx, dest), ...copied, sourceSha256: memberSha256 ?? entry.sha256, url: entry.url, cached: fromCache, externalBin: false, ...(entry.extract ? { archiveSha256: entry.sha256, member: entry.extract, memberSha256 } : {}), ...(entry.extractKind ? { extractKind: entry.extractKind } : {}) }];
|
|
1466
1585
|
} finally {
|
|
1467
1586
|
if (scratch) ctx.removeDir(scratch);
|
|
1468
1587
|
}
|
|
@@ -1550,7 +1669,7 @@ function stageCmake(entry, ctx) {
|
|
|
1550
1669
|
const { appRoot, fs, run, env, target } = ctx;
|
|
1551
1670
|
const cmake = resolveCmakeBinary(ctx, entry);
|
|
1552
1671
|
const generator = entry.platformGenerators?.[target.platform] ?? entry.generator;
|
|
1553
|
-
const root = path.resolve(
|
|
1672
|
+
const root = path.resolve(ctx.cacheRoot, CMAKE_ROOT, entry.name);
|
|
1554
1673
|
const srcDir = path.join(root, "src");
|
|
1555
1674
|
const buildDir = path.join(root, "build");
|
|
1556
1675
|
const dest = destFor(entry, ctx);
|
|
@@ -1687,10 +1806,13 @@ function wipeStaleStageDir(ctx, plan, produced) {
|
|
|
1687
1806
|
* call can reuse (acquireManagedRunLease records a per-process lease file; no RK_* variable carries it to a child), so
|
|
1688
1807
|
* the only safe behaviour is to refuse early. RIGHTKIT_STAGE_SIDECAR_ALLOW_NESTED=1 opts out.
|
|
1689
1808
|
*/
|
|
1690
|
-
export function isNestedBrokerContext(env = process.env) {
|
|
1809
|
+
export function isNestedBrokerContext(env = process.env, platform = process.platform) {
|
|
1810
|
+
// A direct (Linux) host has no broker, so no outer broker build to nest in, whatever flag or socket the env carries (heavy-command sets
|
|
1811
|
+
// RIGHTSUITE_HEAVY_WORK_OWNER on every platform).
|
|
1812
|
+
if (directPlatform(platform)) return false;
|
|
1691
1813
|
if (env?.RIGHTKIT_STAGE_SIDECAR_ALLOW_NESTED === "1") return false;
|
|
1692
1814
|
const insideTauri = Boolean(env?.TAURI_ENV_PLATFORM || env?.TAURI_ENV_TARGET_TRIPLE);
|
|
1693
|
-
return insideTauri && (managedHost(env ?? {}) || env?.RIGHTSUITE_HEAVY_WORK_OWNER === "1");
|
|
1815
|
+
return insideTauri && (managedHost(env ?? {}, platform) || env?.RIGHTSUITE_HEAVY_WORK_OWNER === "1");
|
|
1694
1816
|
}
|
|
1695
1817
|
|
|
1696
1818
|
/** True when staging would call `rustright cargo` for a non-skipped entry. */
|
|
@@ -1734,15 +1856,22 @@ export function planSidecarStaging({ manifest, target, env = process.env } = {})
|
|
|
1734
1856
|
* fs (existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync, renameSync, chmodSync, statSync, readdirSync, rmSync), run, fetch (https downloads of `remote`), fetchBytes, extractMember, loadMediaCatalog, loadNativeCatalog, makeTempDir, removeDir, now, pid, log (progress), warn (warnings; defaults to log), env.
|
|
1735
1857
|
* The API is quiet by default; the `right-release stage-sidecar` command passes `warn` so the CLI prints warnings on stderr.
|
|
1736
1858
|
*/
|
|
1737
|
-
export async function stageSidecars({ appRoot, manifest, target, deps = {} } = {}) {
|
|
1859
|
+
export async function stageSidecars({ appRoot, manifest, target, cacheRoot, deps = {} } = {}) {
|
|
1738
1860
|
if (typeof appRoot !== "string" || !path.isAbsolute(appRoot)) fail("STAGE_SIDECAR_CONFIG_INVALID", "appRoot must be an absolute path");
|
|
1861
|
+
if (cacheRoot !== undefined && (typeof cacheRoot !== "string" || !path.isAbsolute(cacheRoot))) fail("STAGE_SIDECAR_CONFIG_INVALID", "cacheRoot must be an absolute path");
|
|
1739
1862
|
const env = deps.env ?? process.env;
|
|
1740
1863
|
const plan = validateSidecarManifest(manifest, { target, mode: resolveBuildMode(env) });
|
|
1741
1864
|
assertNotNested(plan, env);
|
|
1865
|
+
// A source split (source-split.mjs; rightkit-gate's snapshot route): `appRoot` is the snapshot of the commit, where the declared
|
|
1866
|
+
// sources are read and where the staged files and the receipt go (the Tauri build script reads them there). `cacheRoot` is the
|
|
1867
|
+
// checkout's app directory, which keeps every cache, archive, slice and build tree right-release owns (`.right-release/...`) and
|
|
1868
|
+
// where Cargo's target directory is resolved: only final outputs and the receipt are written under the snapshot (row 7.05a). Checkout
|
|
1869
|
+
// bytes enter a split run only as cache entries whose digest the commit pins, or as tool-invalidated build caches.
|
|
1870
|
+
const split = cacheRoot !== undefined && path.resolve(cacheRoot) !== path.resolve(appRoot);
|
|
1742
1871
|
const fs = deps.fs ?? nodeFs;
|
|
1743
1872
|
const run = deps.run ?? defaultRun;
|
|
1744
1873
|
const ctx = {
|
|
1745
|
-
appRoot, target: plan.target, stageDir: plan.stageDir, fs, env, run, protectedPaths: protectedOutputPaths(plan),
|
|
1874
|
+
appRoot, cacheRoot: split ? path.resolve(cacheRoot) : appRoot, split, target: plan.target, stageDir: plan.stageDir, fs, env, run, protectedPaths: protectedOutputPaths(plan),
|
|
1746
1875
|
windows: plan.target.os === "windows",
|
|
1747
1876
|
pid: deps.pid ?? process.pid,
|
|
1748
1877
|
host: deps.host ?? { os: { darwin: "macos", win32: "windows", linux: "linux" }[process.platform] ?? process.platform, arch: { arm64: "aarch64", x64: "x86_64" }[process.arch] ?? process.arch },
|
|
@@ -1754,6 +1883,7 @@ export async function stageSidecars({ appRoot, manifest, target, deps = {} } = {
|
|
|
1754
1883
|
fetchBytes: deps.fetchBytes ?? defaultFetchBytes,
|
|
1755
1884
|
fetch: deps.fetch ?? ((...args) => globalThis.fetch(...args)),
|
|
1756
1885
|
extractMember: deps.extractMember ?? ((args) => defaultExtractMember({ ...args, run })),
|
|
1886
|
+
extractNsisMember: deps.extractNsisMember ?? ((args) => defaultExtractNsisMember({ ...args, run, env, sevenZip: deps.sevenZip, readFile: fs.readFileSync, exists: fs.existsSync })),
|
|
1757
1887
|
loadMediaCatalog: deps.loadMediaCatalog ?? (() => undefined),
|
|
1758
1888
|
loadNativeCatalog: deps.loadNativeCatalog ?? (() => undefined),
|
|
1759
1889
|
makeTempDir: deps.makeTempDir ?? (() => nodeFs.mkdtempSync(path.join(os.tmpdir(), "rightkit-stage-"))),
|
package/swap-helper-bundle.mjs
CHANGED
|
@@ -18,7 +18,10 @@ export const SWAP_HELPER_PACKAGE = "@rightkit/release";
|
|
|
18
18
|
export const SWAP_HELPER_BIN = ["bin", "darwin", "rightkit-swap"];
|
|
19
19
|
export const SWAP_HELPER_RECEIPT = ["bin", "darwin", "rightkit-swap.sign.json"];
|
|
20
20
|
// rightkit-swap is gated on the crate feature "fs" (crates/rightkit-fs/Cargo.toml [[bin]] required-features).
|
|
21
|
-
|
|
21
|
+
// Every crate is its own Cargo workspace (rightkit#1285): there is no root manifest, so the build and the target-directory read name crates/rightkit-fs.
|
|
22
|
+
export const SWAP_HELPER_MANIFEST = "crates/rightkit-fs/Cargo.toml";
|
|
23
|
+
export const SWAP_HELPER_CARGO_ARGS = ["build", "--manifest-path", SWAP_HELPER_MANIFEST, "--release", "-p", "rightkit-fs", "--features", "fs", "--bin", "rightkit-swap"];
|
|
24
|
+
export const SWAP_HELPER_METADATA_ARGS = ["metadata", "--format-version", "1", "--no-deps", "--manifest-path", SWAP_HELPER_MANIFEST];
|
|
22
25
|
|
|
23
26
|
/** Typed error. Codes: SWAP_HELPER_BUILD_FAILED, SWAP_HELPER_SIGN_FAILED, SWAP_HELPER_MISSING, SWAP_HELPER_UNSIGNED. */
|
|
24
27
|
export class SwapHelperError extends Error {
|
|
@@ -67,7 +70,7 @@ export function prepareSwapHelper({
|
|
|
67
70
|
const fail = (code, message) => { throw new SwapHelperError(code, message); };
|
|
68
71
|
const built = run("cargo", SWAP_HELPER_CARGO_ARGS, { cwd: workspaceRoot, env });
|
|
69
72
|
if (built.error || built.status !== 0) fail("SWAP_HELPER_BUILD_FAILED", `rustright cargo ${SWAP_HELPER_CARGO_ARGS.join(" ")} failed (${built.error?.message ?? `exit ${built.status}`})\n${tail(`${built.stdout ?? ""}\n${built.stderr ?? ""}`)}`);
|
|
70
|
-
const meta = run("cargo",
|
|
73
|
+
const meta = run("cargo", SWAP_HELPER_METADATA_ARGS, { cwd: workspaceRoot, env });
|
|
71
74
|
let targetDir;
|
|
72
75
|
try { targetDir = JSON.parse(meta.stdout).target_directory; } catch { /* refused below */ }
|
|
73
76
|
if (meta.status !== 0 || typeof targetDir !== "string" || !path.isAbsolute(targetDir)) fail("SWAP_HELPER_BUILD_FAILED", "cannot read the Cargo target directory to find rightkit-swap");
|
package/test-command.mjs
CHANGED
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
// from the app root. `--suite journeys` selects every chunk of that group. `--runner vitest -- run --project unit` runs
|
|
22
22
|
// the runner once with the arguments after `--` (the watch or one-off case); `-- <args>` after `--suite` is allowed when
|
|
23
23
|
// exactly one suite is selected. Exit code: 0 when every selected suite passed, else the failing suite's exit code.
|
|
24
|
-
import { readFileSync } from "node:fs";
|
|
24
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
25
|
+
import { buildLaunch } from "./build-launch.mjs";
|
|
25
26
|
import path from "node:path";
|
|
26
27
|
import { pathToFileURL } from "node:url";
|
|
27
28
|
import { spawn } from "./background-process.mjs";
|
|
@@ -39,10 +40,11 @@ export function parseTestArgs(argv) {
|
|
|
39
40
|
for (let i = 0; i < argv.length; i += 1) {
|
|
40
41
|
const arg = argv[i];
|
|
41
42
|
if (arg === "--") { opts.passthrough = argv.slice(i + 1); break; }
|
|
42
|
-
if (arg === "--config" || arg === "--suite" || arg === "--runner") {
|
|
43
|
+
if (arg === "--config" || arg === "--suite" || arg === "--runner" || arg === "--platform") {
|
|
43
44
|
const value = argv[i + 1];
|
|
44
45
|
if (!value || value.startsWith("--")) throw new Error(`${arg} needs a value`);
|
|
45
46
|
if (arg === "--suite") opts.suites.push(...value.split(",").filter(Boolean));
|
|
47
|
+
else if (arg === "--platform") opts.platform = value;
|
|
46
48
|
else opts[arg.slice(2)] = value;
|
|
47
49
|
i += 1;
|
|
48
50
|
} else if (arg === "--list") opts.list = true;
|
|
@@ -147,14 +149,16 @@ export function eventPrefix(config) {
|
|
|
147
149
|
* Run the planned suites in order; stop at the first non-zero exit. `spawnFn` and `log` are injectable.
|
|
148
150
|
* Resolves `{ exitCode, results: [{ suite, exitCode, durationMs }] }`; a spawn error or a signal rejects.
|
|
149
151
|
*/
|
|
150
|
-
export async function runSuites(plan, { appRoot, prefix = "test", env = process.env, spawnFn = spawn, log = console.log } = {}) {
|
|
152
|
+
export async function runSuites(plan, { appRoot, prefix = "test", env = process.env, spawnFn = spawn, log = console.log, door = buildLaunch } = {}) {
|
|
151
153
|
const results = [];
|
|
152
154
|
for (const item of plan) {
|
|
153
155
|
const invocation = resolveInvocation(item.step, { appRoot, baseEnv: env, extraArgs: item.extraArgs });
|
|
154
156
|
log(JSON.stringify({ event: `${prefix}_suite_started`, suite: item.name }));
|
|
155
157
|
const started = Date.now();
|
|
156
158
|
const exitCode = await new Promise((resolve, reject) => {
|
|
157
|
-
|
|
159
|
+
// A test suite goes through the one door (build-launch.mjs): a broker job on a managed host, which builds it under the app's claim.
|
|
160
|
+
const target = door(invocation.cmd, invocation.args, { env: invocation.env, cwd: invocation.cwd, exists: existsSync });
|
|
161
|
+
const child = spawnFn(target.command, target.args, { cwd: invocation.cwd, env: invocation.env, stdio: "inherit" });
|
|
158
162
|
child.once("error", reject);
|
|
159
163
|
child.once("exit", (code, signal) => {
|
|
160
164
|
if (signal) reject(new Error(`${item.name} stopped by ${signal}`));
|
|
@@ -172,6 +176,15 @@ export async function runSuites(plan, { appRoot, prefix = "test", env = process.
|
|
|
172
176
|
export async function main(argv = process.argv.slice(2)) {
|
|
173
177
|
const opts = parseTestArgs(argv);
|
|
174
178
|
const { config, appRoot } = await loadAppConfig(opts.config);
|
|
179
|
+
// `--platform ios`: the app's declared XCUITest journeys (development.targets.ios.uiTests) on its dev device or simulator.
|
|
180
|
+
if (opts.platform !== undefined) {
|
|
181
|
+
if (opts.platform !== "ios") throw new Error(`--platform ${opts.platform}: only ios has platform UI test journeys; other suites are declared in tests.suites`);
|
|
182
|
+
if (opts.suites.length || opts.runner || opts.passthrough.length) throw new Error("--platform ios runs the declared journeys; it takes no --suite, --runner or arguments");
|
|
183
|
+
const { runDevelopment } = await import("./development.mjs");
|
|
184
|
+
const result = runDevelopment({ root: appRoot, platform: "ios", config, uiTests: true, dryRun: opts.list });
|
|
185
|
+
if (opts.list) console.log(JSON.stringify(result.plan ?? result, null, 2));
|
|
186
|
+
return 0;
|
|
187
|
+
}
|
|
175
188
|
const plan = planTests(config, { suites: opts.suites, runner: opts.runner, passthrough: opts.passthrough, appRoot });
|
|
176
189
|
if (opts.list) {
|
|
177
190
|
const view = plan.map((item) => ({ suite: item.name, ...resolveInvocation(item.step, { appRoot, baseEnv: {}, extraArgs: item.extraArgs }), env: undefined }));
|