@rightkit/release 0.6.1 → 0.6.2
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 +12 -0
- package/bin/darwin/rightkit-swap +0 -0
- package/managed-target-stage.mjs +138 -0
- package/package-recipe-targets.mjs +57 -17
- package/package.json +1 -1
- package/recipe-build.mjs +82 -5
- package/release-switch-scan.mjs +47 -3
- package/release.mjs +3 -3
- package/rightkit-versions.json +10 -8
package/README.md
CHANGED
|
@@ -169,6 +169,18 @@ Copies each `targets.<platform>.mirror` entry (an app-relative path, or `{ file,
|
|
|
169
169
|
never writes into the repository root. `pruneSuffix` then removes the other files with that suffix from that directory, so it
|
|
170
170
|
holds only the current installer. The one-file form (`--file <path> --package-root <dir> [--name <name>]`) is unchanged.
|
|
171
171
|
|
|
172
|
+
### Broker-managed Cargo target directory (`cargoTargetRoot`)
|
|
173
|
+
|
|
174
|
+
With `cargoTargetRoot: true` (or an app-relative Cargo.toml path) in `winPackage` / `macPackage`, declared artifact paths under `<cargo dir>/target/` follow `cargo metadata`'s target directory. On a RustRight-managed host that directory is owned by the broker and lies outside the app (for example `D:\.rightkit-managed\rightkit-build-control\ws\<hash>\g\<n>\t`); release.mjs needs app-relative paths, so the recipe keeps the declared paths at their default location under the app's ignored `src-tauri/target/` and plans verified copies (`managed-target-stage.mjs`, the `stage` section of the recipe-build plan):
|
|
175
|
+
|
|
176
|
+
- after `tauri build`, the raw EXE (win) or the built `.app` (mac) is copied from the real target directory to the app-relative path; every later step (sign, package, seal) sees app-relative paths unchanged;
|
|
177
|
+
- win: `tauri bundle` embeds the EXE it finds in the real target directory, so the signed app-relative EXE is copied back there first, and the installer is copied out afterwards (its source is deleted before the bundle step, so a leftover can never be copied);
|
|
178
|
+
- every copy deletes a stale destination first, writes a temp file beside the destination, verifies sha256 (a directory: a digest of every entry, symlinks kept) against the source before and after the copy, then renames; a source the build did not produce fails the step with `STAGE_SOURCE_MISSING` and no stale copy is left to package.
|
|
179
|
+
|
|
180
|
+
Safeguards on the managed Windows package step (a temporary bridge until the broker exposes artifacts into the app; a RustRight issue tracks that): the real raw EXE must equal the signed in-app EXE by sha256 right after the copy-back and again after `tauri bundle` returns (`STAGE_REAL_CHANGED`: another cargo job in the same workspace may have relinked it), and the installer is copied out only after both checks. A copy whose destination is in the broker tree never creates its parent (`STAGE_PARENT_MISSING`: the generation was rotated or evicted). The recipe re-resolves the Cargo target directory through cargo metadata before and after the step and refuses when it differs from the directory planned at expansion (`STAGE_TARGET_DIR_CHANGED`; docs/RIGHTKIT-THIN-BUILD-CONTROL.md §5 and §15).
|
|
181
|
+
|
|
182
|
+
Only a target directory under a managed root (`resolveManagedRoots`: `RIGHTKIT_DATA_ROOT`, `RIGHTKIT_MANAGED_ROOT`, `RIGHTKIT_EXTERNAL_ROOT`, `RIGHTKIT_CONTROL_ROOT`, the legacy `D:\.rightkit-managed` / `/Volumes/D/.rightkit-managed`) is staged. Any other directory outside the app still fails closed: `the Cargo target directory <dir> is outside the app, so <path> cannot be addressed by an app-relative path; point CARGO_TARGET_DIR inside the app or declare the artifact paths yourself`.
|
|
183
|
+
|
|
172
184
|
### macOS updater tarball from `macPackage`
|
|
173
185
|
|
|
174
186
|
`targets.mac.macPackage.updater: true` (or an app-relative `.tar.gz` path) adds a `create-mac-updater` command at the front of
|
package/bin/darwin/rightkit-swap
CHANGED
|
Binary file
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// Broker-managed Cargo target directories (bogusyogi/rightkit#924). On a RustRight-managed host the broker owns the
|
|
2
|
+
// Cargo target directory outside the app, but release.mjs addresses every artifact by an app-relative path (no `..`).
|
|
3
|
+
// A recipe whose Cargo target directory is outside the app AND under a managed root therefore stages its declared
|
|
4
|
+
// artifacts: after the build they are copied from the real target directory to their default app-relative location,
|
|
5
|
+
// and before a later step that reads the signed raw EXE from the target directory (tauri bundle) the signed copy is
|
|
6
|
+
// copied back. A `fresh` source is deleted before the build and every source is polled until stable before it is
|
|
7
|
+
// copied, so only this build's finished output is staged. Every copy is atomic (temp file beside the destination, then rename) and verified by sha256 against its
|
|
8
|
+
// source; a destination in the broker tree is never created (its parent must exist) and the target directory is re-resolved
|
|
9
|
+
// and must be the planned one. This is a temporary bridge until the broker exposes artifacts into the app. A stale destination is deleted first, so a failed copy can never leave an old artifact to be signed or sealed.
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
import { cpSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, realpathSync, renameSync, rmSync } from "node:fs";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { managedControlCandidates } from "./managed-roots.mjs";
|
|
14
|
+
|
|
15
|
+
export class ManagedTargetStageError extends Error {
|
|
16
|
+
constructor(code, message, details = {}) { super(message); this.name = "ManagedTargetStageError"; this.code = code; this.details = details; }
|
|
17
|
+
}
|
|
18
|
+
const fail = (code, message, details) => { throw new ManagedTargetStageError(code, message, details); };
|
|
19
|
+
|
|
20
|
+
const sha256Of = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
21
|
+
|
|
22
|
+
function realOrSelf(value) {
|
|
23
|
+
try { return realpathSync(value); } catch { return path.resolve(value); }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The directories the RustRight broker owns: the managed data and control roots (and the legacy root). */
|
|
27
|
+
export function defaultManagedRoots(env = process.env) {
|
|
28
|
+
return managedControlCandidates({ env }).filter((root) => typeof root === "string" && path.isAbsolute(root));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** True when `dir` is strictly under one of `roots` (case-insensitive on Windows). Symlinks are resolved when they exist. */
|
|
32
|
+
export function isUnderManagedRoot(dir, roots, { platform = process.platform } = {}) {
|
|
33
|
+
const fold = (value) => (platform === "win32" ? value.toLowerCase() : value);
|
|
34
|
+
const target = fold(realOrSelf(dir));
|
|
35
|
+
return roots.some((root) => {
|
|
36
|
+
if (typeof root !== "string" || !path.isAbsolute(root)) return false;
|
|
37
|
+
const relative = path.relative(fold(realOrSelf(root)), target);
|
|
38
|
+
return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative) && !/^[A-Za-z]:/.test(relative);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A content digest of a file, or of a directory tree (relative path, kind, file sha256, symlink target and executable bit
|
|
44
|
+
* of every entry, sorted). Symlinks are never followed.
|
|
45
|
+
*/
|
|
46
|
+
export function contentDigest(file, fs = {}) {
|
|
47
|
+
const lstat = fs.lstatSync ?? lstatSync;
|
|
48
|
+
const readDir = fs.readdirSync ?? readdirSync;
|
|
49
|
+
const readFile = fs.readFileSync ?? readFileSync;
|
|
50
|
+
const readLink = fs.readlinkSync ?? readlinkSync;
|
|
51
|
+
const stat = lstat(file);
|
|
52
|
+
if (stat.isSymbolicLink()) return { kind: "symlink", sha256: sha256Of(`link:${readLink(file)}`) };
|
|
53
|
+
if (stat.isFile()) return { kind: "file", sha256: sha256Of(readFile(file)) };
|
|
54
|
+
if (!stat.isDirectory()) fail("STAGE_ARTIFACT_UNSUPPORTED", `${file} is neither a file nor a directory`);
|
|
55
|
+
const lines = [];
|
|
56
|
+
const walk = (dir, prefix) => {
|
|
57
|
+
for (const name of readDir(dir).sort()) {
|
|
58
|
+
const full = path.join(dir, name);
|
|
59
|
+
const relative = prefix ? `${prefix}/${name}` : name;
|
|
60
|
+
const entry = lstat(full);
|
|
61
|
+
if (entry.isSymbolicLink()) lines.push(`l ${relative} ${readLink(full)}`);
|
|
62
|
+
else if (entry.isDirectory()) { lines.push(`d ${relative}`); walk(full, relative); }
|
|
63
|
+
else if (entry.isFile()) lines.push(`f ${relative} ${(entry.mode & 0o111) !== 0 ? "x" : "-"} ${sha256Of(readFile(full))}`);
|
|
64
|
+
else fail("STAGE_ARTIFACT_UNSUPPORTED", `${full} is neither a file, a directory nor a symlink`);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
walk(file, "");
|
|
68
|
+
return { kind: "directory", sha256: sha256Of(lines.join("\n")) };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Copy one artifact from `from` to `to` (both absolute): delete a stale destination, check the source exists, copy beside
|
|
73
|
+
* the destination, verify the copy's sha256 against the source (read before and after the copy), then rename into place.
|
|
74
|
+
* Returns { from, to, kind, sha256 }.
|
|
75
|
+
*/
|
|
76
|
+
export function stageArtifact({ from, to }, { fs = {}, log = () => {}, requireParent = false } = {}) {
|
|
77
|
+
const remove = fs.rmSync ?? rmSync;
|
|
78
|
+
const copy = fs.cpSync ?? cpSync;
|
|
79
|
+
const rename = fs.renameSync ?? renameSync;
|
|
80
|
+
const makeDir = fs.mkdirSync ?? mkdirSync;
|
|
81
|
+
// A destination in the broker's tree is never created: a missing parent means the generation was rotated or evicted.
|
|
82
|
+
if (requireParent && !(fs.lstatSync ?? lstatSync)(path.dirname(to), { throwIfNoEntry: false })) {
|
|
83
|
+
fail("STAGE_PARENT_MISSING", `managed target stage: the broker directory ${path.dirname(to)} does not exist (the build-control generation was rotated or evicted); not creating it, ${from} was not copied`, { from, to });
|
|
84
|
+
}
|
|
85
|
+
// A destination left by an earlier build is never an acceptable result: remove it before anything else.
|
|
86
|
+
remove(to, { recursive: true, force: true });
|
|
87
|
+
if (!(fs.lstatSync ?? lstatSync)(from, { throwIfNoEntry: false })) {
|
|
88
|
+
fail("STAGE_SOURCE_MISSING", `managed target stage: the build did not produce ${from}; ${to} was removed and nothing was copied, so no stale artifact can be packaged`, { from, to });
|
|
89
|
+
}
|
|
90
|
+
const before = contentDigest(from, fs);
|
|
91
|
+
const temp = path.join(path.dirname(to), `.${path.basename(to)}.stage-${process.pid}-${Date.now()}.tmp`);
|
|
92
|
+
makeDir(path.dirname(to), { recursive: true });
|
|
93
|
+
remove(temp, { recursive: true, force: true });
|
|
94
|
+
try {
|
|
95
|
+
copy(from, temp, { recursive: true, verbatimSymlinks: true, preserveTimestamps: true, force: false, errorOnExist: true });
|
|
96
|
+
const copied = contentDigest(temp, fs);
|
|
97
|
+
const after = contentDigest(from, fs);
|
|
98
|
+
if (copied.sha256 !== before.sha256 || after.sha256 !== before.sha256) {
|
|
99
|
+
fail("STAGE_DIGEST_MISMATCH", `managed target stage: sha256 of the copy of ${from} does not match its source (source ${before.sha256}, copy ${copied.sha256}, source after copy ${after.sha256}); ${to} was not created`, { from, to });
|
|
100
|
+
}
|
|
101
|
+
rename(temp, to);
|
|
102
|
+
} finally {
|
|
103
|
+
remove(temp, { recursive: true, force: true });
|
|
104
|
+
}
|
|
105
|
+
log(`recipe-build: staged ${from} -> ${to} (${before.kind} sha256 ${before.sha256})`);
|
|
106
|
+
return { from, to, kind: before.kind, sha256: before.sha256 };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The two files (or trees) must have the same sha256; `what` names the check in the message. */
|
|
110
|
+
export function verifySameContent({ app, real }, { what, fs = {} } = {}) {
|
|
111
|
+
const a = contentDigest(app, fs).sha256;
|
|
112
|
+
let b;
|
|
113
|
+
try { b = contentDigest(real, fs).sha256; } catch (error) { fail("STAGE_REAL_CHANGED", `managed target stage (${what}): cannot read ${real} (${error.message})`, { app, real }); }
|
|
114
|
+
if (a !== b) fail("STAGE_REAL_CHANGED", `managed target stage (${what}): ${real} (sha256 ${b}) is no longer the signed ${app} (sha256 ${a}); another build in the same workspace changed it, so nothing was packaged or copied out`, { app, real });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The Cargo target directory must still be the one planned: a broker generation rotation or eviction changes it. */
|
|
118
|
+
export function assertTargetDirUnchanged({ manifest, expected }, resolveTargetRoot, { what } = {}) {
|
|
119
|
+
let now;
|
|
120
|
+
try { now = resolveTargetRoot(manifest); } catch (error) { fail("STAGE_TARGET_DIR_CHANGED", `managed target stage (${what}): cannot re-resolve the Cargo target directory (${error.message})`, { manifest, expected }); }
|
|
121
|
+
const same = (x, y) => (process.platform === "win32" ? path.resolve(x).toLowerCase() === path.resolve(y).toLowerCase() : path.resolve(x) === path.resolve(y));
|
|
122
|
+
if (!same(now, expected)) fail("STAGE_TARGET_DIR_CHANGED", `managed target stage (${what}): the Cargo target directory is now ${now}, not the ${expected} planned for this release (broker generation rotated or evicted); refusing to copy`, { manifest, expected, now });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Remove the sources named `fresh` before a build step, so a missing output after it cannot be a leftover. */
|
|
126
|
+
export function removeFreshSources(entries, { fs = {}, log = () => {} } = {}) {
|
|
127
|
+
const remove = fs.rmSync ?? rmSync;
|
|
128
|
+
for (const entry of entries) {
|
|
129
|
+
if (!entry.fresh) continue;
|
|
130
|
+
log(`recipe-build: stage removes stale source ${entry.from}`);
|
|
131
|
+
remove(entry.from, { recursive: true, force: true });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Stage every entry in order; the first failure throws and nothing after it is copied. */
|
|
136
|
+
export function stageArtifacts(entries, options = {}) {
|
|
137
|
+
return entries.map((entry) => stageArtifact(entry, options));
|
|
138
|
+
}
|
|
@@ -8,6 +8,7 @@ import { existsSync } from "node:fs";
|
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
import { resolveTargetRoot as cargoResolveTargetRoot } from "./cargo-target.mjs";
|
|
11
|
+
import { defaultManagedRoots, isUnderManagedRoot } from "./managed-target-stage.mjs";
|
|
11
12
|
import { validateWinPackage, WinPackageConfigError } from "./win-package-recipe.mjs";
|
|
12
13
|
import { IOS_SIGNING_CONTRACT, IosPackageError, validateIosPackageConfig } from "./ios-package-recipe.mjs";
|
|
13
14
|
import { IosUploadError, validateIosDistribution } from "./ios-upload.mjs";
|
|
@@ -77,13 +78,13 @@ const configArgs = (extras, appRoot, cwd) => (extras.tauriConfig ? ["--config",
|
|
|
77
78
|
* release.mjs runs ONE prePackage command: node recipe-build.mjs runs the built-in cleanup, prepare, the Tauri build,
|
|
78
79
|
* the artifact stability poll and postBuild in order. `cleanup` and `stability` are recipe-build plan sections.
|
|
79
80
|
*/
|
|
80
|
-
function recipeBuildCommand({ extras, build, env, cleanup, stability, timeoutMs }) {
|
|
81
|
+
function recipeBuildCommand({ extras, build, env, cleanup, stability, stage, timeoutMs }) {
|
|
81
82
|
const steps = [
|
|
82
83
|
...extras.prepare.map((step) => ({ phase: "prepare", ...step })),
|
|
83
84
|
{ phase: "build", cmd: build.cmd, args: build.args, cwd: build.cwd },
|
|
84
85
|
...extras.postBuild.map((step) => ({ phase: "postBuild", ...step })),
|
|
85
86
|
];
|
|
86
|
-
const plan = { version: 1, steps, ...(cleanup ? { cleanup } : {}), ...(stability ? { stability } : {}) };
|
|
87
|
+
const plan = { version: 1, steps, ...(cleanup ? { cleanup } : {}), ...(stability ? { stability } : {}), ...(stage && (stage.pre?.length || stage.post?.length) ? { stage } : {}) };
|
|
87
88
|
return { cmd: process.execPath, args: [RECIPE_BUILD, "--plan", JSON.stringify(plan)], env, ...(timeoutMs ? { timeoutMs } : {}) };
|
|
88
89
|
}
|
|
89
90
|
|
|
@@ -95,9 +96,21 @@ const positiveInt = (value) => Number.isInteger(value) && value > 0;
|
|
|
95
96
|
* With `cargoTargetRoot` (true: <tauriCwd>/src-tauri/Cargo.toml; or an app-relative Cargo.toml path) or CARGO_TARGET_DIR set
|
|
96
97
|
* (default manifest), `cargo metadata`'s target_directory is resolved and every declared path under the default
|
|
97
98
|
* `<cargo dir>/target/` is moved to the real directory. release.mjs needs app-relative paths with no `..`, so a target
|
|
98
|
-
* directory outside the app fails closed (use RIGHT_RELEASE_ARTIFACT_DIR for copies, see the docs)
|
|
99
|
+
* directory outside the app fails closed (use RIGHT_RELEASE_ARTIFACT_DIR for copies, see the docs), EXCEPT a directory under
|
|
100
|
+
* a broker-managed root (the RustRight data root, bogusyogi/rightkit#924): the declared path then stays at its default
|
|
101
|
+
* app-relative location and the mapper records `{ from: <real path>, to: <declared path> }` so the caller plans a verified
|
|
102
|
+
* copy step (managed-target-stage.mjs). `mapPath.staged` lists the recorded entries; `mapPath.sourceOf(declared)` finds one.
|
|
99
103
|
*/
|
|
100
|
-
function targetRootMapper({ spec, appRoot, cwd, env, resolveTargetRoot, label }) {
|
|
104
|
+
function targetRootMapper({ spec, appRoot, cwd, env, resolveTargetRoot, label, managedRoots = defaultManagedRoots(env) }) {
|
|
105
|
+
const staged = new Map();
|
|
106
|
+
const mapper = buildTargetRootMapper({ spec, appRoot, cwd, env, resolveTargetRoot, label, managedRoots, staged });
|
|
107
|
+
mapper.staged = staged;
|
|
108
|
+
mapper.sourceOf = (declared) => staged.get(declared);
|
|
109
|
+
mapper.resolve = () => staged.resolve;
|
|
110
|
+
return mapper;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function buildTargetRootMapper({ spec, appRoot, cwd, env, resolveTargetRoot, label, managedRoots, staged }) {
|
|
101
114
|
const explicit = spec.cargoTargetRoot;
|
|
102
115
|
if (explicit !== undefined && explicit !== true && explicit !== false && typeof explicit !== "string") fail(`${label}.cargoTargetRoot must be true or an app-relative Cargo.toml path`);
|
|
103
116
|
const envDir = typeof env.CARGO_TARGET_DIR === "string" && env.CARGO_TARGET_DIR.trim() !== "";
|
|
@@ -114,19 +127,26 @@ function targetRootMapper({ spec, appRoot, cwd, env, resolveTargetRoot, label })
|
|
|
114
127
|
const moved = path.resolve(targetDir, normalized.slice(prefix.length + 1));
|
|
115
128
|
const relative = path.relative(appRoot, moved).split(path.sep).join("/");
|
|
116
129
|
if (relative === "" || relative.startsWith("..") || path.isAbsolute(relative) || /^[A-Za-z]:/.test(relative)) {
|
|
130
|
+
if (isUnderManagedRoot(targetDir, managedRoots)) {
|
|
131
|
+
if (normalized.split("/").includes("..")) fail(`${label}: ${declared} must not contain ..`);
|
|
132
|
+
// The default app-relative location under the app's ignored target directory; the copy is planned by the caller.
|
|
133
|
+
staged.set(normalized, { from: moved, to: normalized });
|
|
134
|
+
staged.resolve = { manifest, expected: targetDir };
|
|
135
|
+
return normalized;
|
|
136
|
+
}
|
|
117
137
|
fail(`${label}: the Cargo target directory ${targetDir} is outside the app, so ${declared} cannot be addressed by an app-relative path; point CARGO_TARGET_DIR inside the app or declare the artifact paths yourself`);
|
|
118
138
|
}
|
|
119
139
|
return relative;
|
|
120
140
|
};
|
|
121
141
|
}
|
|
122
142
|
|
|
123
|
-
function winPrePackage({ extras, cwd, appRoot, triple, jobs, timeoutMs, cleanup, stability }) {
|
|
143
|
+
function winPrePackage({ extras, cwd, appRoot, triple, jobs, timeoutMs, cleanup, stability, stage }) {
|
|
124
144
|
const args = ["exec", "tauri", "build", "--no-bundle", "--target", triple, ...configArgs(extras, appRoot, cwd), ...buildCargoArgs(extras, jobs)];
|
|
125
|
-
return recipeBuildCommand({ extras, build: { cmd: "pnpm", args, cwd }, env: QUIET_TAURI_ENV, cleanup, stability, timeoutMs });
|
|
145
|
+
return recipeBuildCommand({ extras, build: { cmd: "pnpm", args, cwd }, env: QUIET_TAURI_ENV, cleanup, stability, stage, timeoutMs });
|
|
126
146
|
}
|
|
127
147
|
|
|
128
148
|
/** `targets.win.winPackage` → prePackage (`tauri build --no-bundle`) and package (`tauri bundle --bundles nsis`). */
|
|
129
|
-
export function expandWinPackageTarget(target, { appRoot, config = {}, env = process.env, resolveTargetRoot = cargoResolveTargetRoot }) {
|
|
149
|
+
export function expandWinPackageTarget(target, { appRoot, config = {}, env = process.env, resolveTargetRoot = cargoResolveTargetRoot, managedRoots }) {
|
|
130
150
|
if (!target?.winPackage) return withoutRecipe(target);
|
|
131
151
|
if (target.prePackage !== undefined) fail("targets.win.winPackage replaces prePackage; remove the prePackage script");
|
|
132
152
|
if (target.winPackage.sidecars !== undefined) fail("targets.win.winPackage.sidecars is not supported in a right-release target: declare sidecars through externalBin or mediaSidecar, which release.mjs signs with the raw EXE");
|
|
@@ -145,19 +165,35 @@ export function expandWinPackageTarget(target, { appRoot, config = {}, env = pro
|
|
|
145
165
|
if (target.signingContract !== undefined && target.signingContract !== "windows-raw-exe-authenticode-before-nsis-v1") fail(`targets.win.winPackage builds an NSIS installer; signingContract must be windows-raw-exe-authenticode-before-nsis-v1, got ${target.signingContract}`);
|
|
146
166
|
const cwd = rel(plan.appRoot, plan.tauriCwd) || ".";
|
|
147
167
|
const override = { bundle: { createUpdaterArtifacts: false, windows: { nsis: { installMode: plan.perMachine ? "perMachine" : "currentUser" } } } };
|
|
148
|
-
const mapPath = targetRootMapper({ spec: target.winPackage, appRoot: plan.appRoot, cwd, env, resolveTargetRoot, label: "targets.win.winPackage" });
|
|
168
|
+
const mapPath = targetRootMapper({ spec: target.winPackage, appRoot: plan.appRoot, cwd, env, resolveTargetRoot, label: "targets.win.winPackage", ...(managedRoots ? { managedRoots } : {}) });
|
|
149
169
|
const rawExe = mapPath(rel(plan.appRoot, plan.rawExe));
|
|
150
170
|
const installerFile = mapPath(rel(plan.appRoot, plan.installerFile));
|
|
151
|
-
// Stale outputs are removed before the build, and the raw EXE is polled until it stops changing after it
|
|
171
|
+
// Stale outputs are removed before the build, and the raw EXE is polled until it stops changing after it (in a managed
|
|
172
|
+
// target directory the poll covers the real EXE before it is copied; the in-app copy is polled again after the copy).
|
|
152
173
|
const cleanup = cleanStale === false ? undefined : { remove: [rawExe, installerFile] };
|
|
153
174
|
const timeout = timeoutMs ?? DEFAULT_WIN_TIMEOUT_MS;
|
|
175
|
+
// Broker-managed target directory: the real raw EXE is deleted before the build (it holds the previous signed copy) and
|
|
176
|
+
// the fresh one is copied out once stable; after signing, the signed copy goes back
|
|
177
|
+
// for `tauri bundle` (which embeds the EXE it finds in the real target directory) and the installer is copied out.
|
|
178
|
+
const rawSource = mapPath.sourceOf(rawExe);
|
|
179
|
+
const installerSource = mapPath.sourceOf(installerFile);
|
|
180
|
+
const prePackageStage = rawSource ? { post: [{ from: rawSource.from, to: rawExe, fresh: true }], resolve: mapPath.resolve() } : undefined;
|
|
181
|
+
const packageStage = rawSource || installerSource ? {
|
|
182
|
+
...(rawSource ? { pre: [{ from: rawExe, to: rawSource.from }], verify: [{ app: rawExe, real: rawSource.from }] } : {}),
|
|
183
|
+
resolve: mapPath.resolve(),
|
|
184
|
+
...(installerSource ? { post: [{ from: installerSource.from, to: installerFile, fresh: true }] } : {}),
|
|
185
|
+
} : undefined;
|
|
186
|
+
const bundleArgs = ["exec", "tauri", "bundle", "--bundles", "nsis", "--target", plan.triple, ...configArgs(extras, plan.appRoot, cwd), "--config", JSON.stringify(override), ...profileArgs(extras)];
|
|
187
|
+
const bundleCommand = packageStage
|
|
188
|
+
? recipeBuildCommand({ extras: { prepare: [], postBuild: [] }, build: { cmd: "pnpm", args: bundleArgs, cwd }, env: QUIET_TAURI_ENV, stage: packageStage, timeoutMs: timeout })
|
|
189
|
+
: { cmd: "pnpm", args: bundleArgs, cwd, env: QUIET_TAURI_ENV, timeoutMs: timeout };
|
|
154
190
|
// `recipe` is set only here: a hand-written one is dropped so it can never select a recipe path.
|
|
155
191
|
const { winPackage, recipe: _ignoredWin, ...rest } = target;
|
|
156
192
|
return {
|
|
157
193
|
...rest,
|
|
158
194
|
signingContract: "windows-raw-exe-authenticode-before-nsis-v1",
|
|
159
|
-
prePackage: winPrePackage({ extras, cwd, appRoot: plan.appRoot, triple: plan.triple, jobs, timeoutMs: timeout, cleanup, stability: { files: [rawExe] } }),
|
|
160
|
-
package:
|
|
195
|
+
prePackage: winPrePackage({ extras, cwd, appRoot: plan.appRoot, triple: plan.triple, jobs, timeoutMs: timeout, cleanup, stability: { files: [rawExe] }, stage: prePackageStage }),
|
|
196
|
+
package: bundleCommand,
|
|
161
197
|
sign: { ...(target.sign ?? {}), prePackageFiles: [rawExe], files: [installerFile] },
|
|
162
198
|
recipe: { kind: "winPackage", productName: plan.productName, updater: plan.updater },
|
|
163
199
|
};
|
|
@@ -185,7 +221,7 @@ function macCleanupSpec(value) {
|
|
|
185
221
|
* `updater: true | "<app-relative .tar.gz>"` emits the Tauri updater tarball and its signature from the signed, stapled .app:
|
|
186
222
|
* it prepends a `create-mac-updater` command to `postSign` (default output `<App>.app.tar.gz` beside the app's working root).
|
|
187
223
|
*/
|
|
188
|
-
export function expandMacPackageTarget(target, { appRoot, env = process.env, resolveTargetRoot = cargoResolveTargetRoot }) {
|
|
224
|
+
export function expandMacPackageTarget(target, { appRoot, env = process.env, resolveTargetRoot = cargoResolveTargetRoot, managedRoots }) {
|
|
189
225
|
if (!target?.macPackage) return withoutRecipe(target);
|
|
190
226
|
const spec = target.macPackage;
|
|
191
227
|
if (!isObject(spec)) fail("targets.mac.macPackage must be an object");
|
|
@@ -214,7 +250,7 @@ export function expandMacPackageTarget(target, { appRoot, env = process.env, res
|
|
|
214
250
|
const args = ["exec", "tauri", "build", "--target", triple, "--bundles", "app", "--no-sign", ...configArgs(extras, appRoot, cwd), "--config", JSON.stringify(override)];
|
|
215
251
|
if (spec.features?.length) args.push("--features", spec.features.join(","));
|
|
216
252
|
args.push(...profileArgs(extras));
|
|
217
|
-
const mapPath = targetRootMapper({ spec, appRoot, cwd, env, resolveTargetRoot, label: "targets.mac.macPackage" });
|
|
253
|
+
const mapPath = targetRootMapper({ spec, appRoot, cwd, env, resolveTargetRoot, label: "targets.mac.macPackage", ...(managedRoots ? { managedRoots } : {}) });
|
|
218
254
|
const app = mapPath(target.notarize.app);
|
|
219
255
|
const entryFile = (entry) => (typeof entry === "string" ? entry : entry?.file);
|
|
220
256
|
const declaredFiles = target.sign?.prePackageFiles ?? [target.notarize.app];
|
|
@@ -230,7 +266,11 @@ export function expandMacPackageTarget(target, { appRoot, env = process.env, res
|
|
|
230
266
|
...(cleanupSpec.staleApps || cleanupSpec.lsclean ? { apps: { dir: path.posix.dirname(appPosix), remove: cleanupSpec.staleApps === true, unregister: cleanupSpec.lsclean === true } } : {}),
|
|
231
267
|
}
|
|
232
268
|
: undefined;
|
|
233
|
-
|
|
269
|
+
// Broker-managed target directory: the built .app (and any other declared file under the target) is copied to its app-relative location.
|
|
270
|
+
// An entry inside another staged entry (a file within the .app) is covered by copying the outer one.
|
|
271
|
+
const macEntries = [...mapPath.staged.values()].filter((entry) => ![...mapPath.staged.values()].some((outer) => outer !== entry && entry.to.startsWith(`${outer.to}/`)));
|
|
272
|
+
const macStage = macEntries.length ? { post: macEntries.map((entry) => ({ ...entry, fresh: true })), resolve: mapPath.resolve() } : undefined;
|
|
273
|
+
const wrapped = extras.prepare.length || extras.postBuild.length || cleanup || macStage;
|
|
234
274
|
const { macPackage, recipe: _ignoredMac, ...rest } = target;
|
|
235
275
|
// The updater tarball is made from the signed .app, so it runs in postSign, ahead of any postSign the app declares.
|
|
236
276
|
const updaterOutput = spec.updater === true ? `${productName}.app.tar.gz` : typeof spec.updater === "string" ? relativeInside(spec.updater, "targets.mac.macPackage.updater") : undefined;
|
|
@@ -243,7 +283,7 @@ export function expandMacPackageTarget(target, { appRoot, env = process.env, res
|
|
|
243
283
|
return {
|
|
244
284
|
...rest,
|
|
245
285
|
...postSign,
|
|
246
|
-
prePackage: wrapped ? recipeBuildCommand({ extras, build: { cmd: "pnpm", args, cwd }, env: QUIET_TAURI_ENV, cleanup }) : { cmd: "pnpm", args, cwd, env: QUIET_TAURI_ENV },
|
|
286
|
+
prePackage: wrapped ? recipeBuildCommand({ extras, build: { cmd: "pnpm", args, cwd }, env: QUIET_TAURI_ENV, cleanup, stage: macStage }) : { cmd: "pnpm", args, cwd, env: QUIET_TAURI_ENV },
|
|
247
287
|
sign: { ...(target.sign ?? {}), prePackageFiles },
|
|
248
288
|
notarize: { ...target.notarize, app, appBeforePackage: true },
|
|
249
289
|
dmg: isObject(target.dmg) ? target.dmg : {},
|
|
@@ -289,8 +329,8 @@ export function expandIosPackageTarget(target, { appRoot: _appRoot } = {}) {
|
|
|
289
329
|
}
|
|
290
330
|
|
|
291
331
|
/** Expand whichever declarative package key the target carries. */
|
|
292
|
-
export function expandPackageRecipeTarget(target, { platform, appRoot, config, env, resolveTargetRoot }) {
|
|
293
|
-
const injected = { ...(env ? { env } : {}), ...(resolveTargetRoot ? { resolveTargetRoot } : {}) };
|
|
332
|
+
export function expandPackageRecipeTarget(target, { platform, appRoot, config, env, resolveTargetRoot, managedRoots }) {
|
|
333
|
+
const injected = { ...(env ? { env } : {}), ...(resolveTargetRoot ? { resolveTargetRoot } : {}), ...(managedRoots ? { managedRoots } : {}) };
|
|
294
334
|
if (platform === "win") return expandWinPackageTarget(target, { appRoot, config, ...injected });
|
|
295
335
|
if (platform === "mac") return expandMacPackageTarget(target, { appRoot, config, ...injected });
|
|
296
336
|
if (platform === "ios") return expandIosPackageTarget(target, { appRoot });
|
package/package.json
CHANGED
package/recipe-build.mjs
CHANGED
|
@@ -19,10 +19,21 @@
|
|
|
19
19
|
// stability: { files: [file], intervalMs?, checks?, timeoutMs? } runs right AFTER the build step: each file must exist
|
|
20
20
|
// and keep the same size and mtime over `checks` consecutive polls `intervalMs` apart (defaults 2 and 1000 ms,
|
|
21
21
|
// timeout 120 s), so a hung or still-writing Tauri process cannot hand a half-written EXE to signing.
|
|
22
|
+
// stage: { pre?: [{ from: app-relative, to: absolute }], post?: [{ from: absolute, to: app-relative, fresh?: bool }] }
|
|
23
|
+
// the broker-managed target directory shape (bogusyogi/rightkit#924, managed-target-stage.mjs): `pre` copies
|
|
24
|
+
// app files into the real target directory right before the build step (the signed raw EXE, which tauri bundle
|
|
25
|
+
// reads from there); `post` waits for each real output to be stable (the stability settings, or their defaults), then copies it (a missing output skips the wait and fails the copy) to its app-relative location right after the build, and with
|
|
26
|
+
// `fresh` the absolute source is deleted before the build step so a missing output cannot be a leftover. Every
|
|
27
|
+
// copy deletes a stale destination first, is atomic, and is verified by sha256; a missing source fails the step.
|
|
28
|
+
// `pre` never creates the broker-side parent directory. `verify: [{ app, real }]` requires the real file to equal the
|
|
29
|
+
// signed app file before and after the build step; `resolve: { manifest, expected }` re-runs cargo metadata and
|
|
30
|
+
// requires the planned target directory before and after it (a temporary bridge until the broker exposes artifacts).
|
|
22
31
|
import { existsSync as nodeExistsSync, readFileSync, readdirSync as nodeReaddirSync, rmSync as nodeRmSync, statSync as nodeStatSync } from "node:fs";
|
|
23
32
|
import path from "node:path";
|
|
24
33
|
import { pathToFileURL } from "node:url";
|
|
25
34
|
import { spawnSync as nodeSpawnSync } from "./background-process.mjs";
|
|
35
|
+
import { resolveTargetRoot as cargoResolveTargetRoot } from "./cargo-target.mjs";
|
|
36
|
+
import { assertTargetDirUnchanged, removeFreshSources, stageArtifacts, verifySameContent } from "./managed-target-stage.mjs";
|
|
26
37
|
|
|
27
38
|
export const RECIPE_BUILD_PLAN_VERSION = 1;
|
|
28
39
|
export const RECIPE_PHASES = Object.freeze(["prepare", "build", "postBuild"]);
|
|
@@ -39,7 +50,9 @@ export class RecipeBuildError extends Error {
|
|
|
39
50
|
const fail = (code, message, details) => { throw new RecipeBuildError(code, message, details); };
|
|
40
51
|
const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
41
52
|
const STEP_KEYS = new Set(["phase", "cmd", "args", "cwd", "env"]);
|
|
42
|
-
const PLAN_KEYS = new Set(["version", "steps", "cleanup", "stability"]);
|
|
53
|
+
const PLAN_KEYS = new Set(["version", "steps", "cleanup", "stability", "stage"]);
|
|
54
|
+
const STAGE_KEYS = new Set(["pre", "post", "verify", "resolve"]);
|
|
55
|
+
const STAGE_ENTRY_KEYS = new Set(["from", "to", "fresh"]);
|
|
43
56
|
const CLEANUP_KEYS = new Set(["remove", "apps", "dmgVolumes"]);
|
|
44
57
|
const APPS_KEYS = new Set(["dir", "remove", "unregister"]);
|
|
45
58
|
const STABILITY_KEYS = new Set(["files", "intervalMs", "checks", "timeoutMs"]);
|
|
@@ -94,6 +107,53 @@ function validateStability(value, errors) {
|
|
|
94
107
|
return { files, ...(value.intervalMs === undefined ? {} : { intervalMs: value.intervalMs }), ...(value.checks === undefined ? {} : { checks: value.checks }), ...(value.timeoutMs === undefined ? {} : { timeoutMs: value.timeoutMs }) };
|
|
95
108
|
}
|
|
96
109
|
|
|
110
|
+
function validateStage(value, errors) {
|
|
111
|
+
if (value === undefined) return undefined;
|
|
112
|
+
if (!isObject(value)) { errors.push("stage must be an object"); return undefined; }
|
|
113
|
+
unknownKeys(value, STAGE_KEYS, "stage", errors);
|
|
114
|
+
const out = {};
|
|
115
|
+
const absolute = (item, label) => {
|
|
116
|
+
if (typeof item !== "string" || !path.isAbsolute(item) || item.split(/[\\/]/).includes("..")) { errors.push(`${label} must be an absolute path without ..: ${JSON.stringify(item)}`); return undefined; }
|
|
117
|
+
return item;
|
|
118
|
+
};
|
|
119
|
+
for (const [side, appKey, outerKey] of [["pre", "from", "to"], ["post", "to", "from"]]) {
|
|
120
|
+
if (value[side] === undefined) continue;
|
|
121
|
+
if (!Array.isArray(value[side])) { errors.push(`stage.${side} must be a list of { from, to }`); continue; }
|
|
122
|
+
out[side] = value[side].map((entry, index) => {
|
|
123
|
+
const label = `stage.${side}[${index}]`;
|
|
124
|
+
if (!isObject(entry)) { errors.push(`${label} must be an object`); return undefined; }
|
|
125
|
+
unknownKeys(entry, STAGE_ENTRY_KEYS, label, errors);
|
|
126
|
+
if (entry.fresh !== undefined && typeof entry.fresh !== "boolean") errors.push(`${label}.fresh must be a boolean`);
|
|
127
|
+
if (side === "pre" && entry.fresh !== undefined) errors.push(`${label}.fresh is only valid in stage.post`);
|
|
128
|
+
const inner = insideApp(entry[appKey], `${label}.${appKey}`, errors, { strict: true });
|
|
129
|
+
const outer = absolute(entry[outerKey], `${label}.${outerKey}`);
|
|
130
|
+
if (!inner || !outer) return undefined;
|
|
131
|
+
return side === "pre" ? { from: inner, to: outer } : { from: outer, to: inner, ...(entry.fresh === true ? { fresh: true } : {}) };
|
|
132
|
+
}).filter(Boolean);
|
|
133
|
+
}
|
|
134
|
+
if (value.verify !== undefined) {
|
|
135
|
+
if (!Array.isArray(value.verify)) errors.push("stage.verify must be a list of { app, real }");
|
|
136
|
+
else out.verify = value.verify.map((entry, index) => {
|
|
137
|
+
const label = `stage.verify[${index}]`;
|
|
138
|
+
if (!isObject(entry)) { errors.push(`${label} must be an object`); return undefined; }
|
|
139
|
+
unknownKeys(entry, new Set(["app", "real"]), label, errors);
|
|
140
|
+
const app = insideApp(entry.app, `${label}.app`, errors, { strict: true });
|
|
141
|
+
const real = absolute(entry.real, `${label}.real`);
|
|
142
|
+
return app && real ? { app, real } : undefined;
|
|
143
|
+
}).filter(Boolean);
|
|
144
|
+
}
|
|
145
|
+
if (value.resolve !== undefined) {
|
|
146
|
+
if (!isObject(value.resolve)) errors.push("stage.resolve must be { manifest, expected }");
|
|
147
|
+
else {
|
|
148
|
+
unknownKeys(value.resolve, new Set(["manifest", "expected"]), "stage.resolve", errors);
|
|
149
|
+
const manifest = absolute(value.resolve.manifest, "stage.resolve.manifest");
|
|
150
|
+
const expected = absolute(value.resolve.expected, "stage.resolve.expected");
|
|
151
|
+
if (manifest && expected) out.resolve = { manifest, expected };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
|
|
97
157
|
/** Validate a plan strictly; every problem is reported at once as RECIPE_BUILD_PLAN_INVALID. Returns the normalized plan. */
|
|
98
158
|
export function validateRecipePlan(plan) {
|
|
99
159
|
const errors = [];
|
|
@@ -123,8 +183,9 @@ export function validateRecipePlan(plan) {
|
|
|
123
183
|
if (builds !== 1) errors.push(`exactly one build step is required, found ${builds}`);
|
|
124
184
|
const cleanup = validateCleanup(plan.cleanup, errors);
|
|
125
185
|
const stability = validateStability(plan.stability, errors);
|
|
186
|
+
const stage = validateStage(plan.stage, errors);
|
|
126
187
|
if (errors.length) fail("RECIPE_BUILD_PLAN_INVALID", `invalid recipe plan:\n${errors.map((line) => `- ${line}`).join("\n")}`, { errors });
|
|
127
|
-
return { version: RECIPE_BUILD_PLAN_VERSION, steps, ...(cleanup ? { cleanup } : {}), ...(stability ? { stability } : {}) };
|
|
188
|
+
return { version: RECIPE_BUILD_PLAN_VERSION, steps, ...(cleanup ? { cleanup } : {}), ...(stability ? { stability } : {}), ...(stage ? { stage } : {}) };
|
|
128
189
|
}
|
|
129
190
|
|
|
130
191
|
// cmd.exe would reinterpret these in a shell launch; refuse rather than escape.
|
|
@@ -258,17 +319,29 @@ export function waitForStableArtifacts(stability, { appRoot, log = () => {}, sta
|
|
|
258
319
|
|
|
259
320
|
/**
|
|
260
321
|
* Run the plan: cleanup, prepare steps, the build step, stability polling, then postBuild steps, strictly in order.
|
|
261
|
-
* Returns { steps: [{ phase, cmd, status }], cleaned, stable }. The first failure throws RECIPE_BUILD_STEP_FAILED and
|
|
322
|
+
* Returns { steps: [{ phase, cmd, status }], cleaned, stable, staged }. The first failure throws RECIPE_BUILD_STEP_FAILED and
|
|
262
323
|
* nothing after it runs.
|
|
263
324
|
* Options: appRoot (absolute), env, platform, log, spawnSync, fs, sleep, now, statSync (injected in tests).
|
|
264
325
|
*/
|
|
265
|
-
export function runRecipeBuild(rawPlan, { appRoot, env = process.env, platform = process.platform, log = () => {}, spawnSync = nodeSpawnSync, fs, sleep, now, statSync } = {}) {
|
|
326
|
+
export function runRecipeBuild(rawPlan, { appRoot, env = process.env, platform = process.platform, log = () => {}, spawnSync = nodeSpawnSync, fs, sleep, now, statSync, resolveTargetRoot = cargoResolveTargetRoot } = {}) {
|
|
266
327
|
if (typeof appRoot !== "string" || !path.isAbsolute(appRoot)) fail("RECIPE_BUILD_PLAN_INVALID", "appRoot must be an absolute path");
|
|
267
328
|
const plan = validateRecipePlan(rawPlan);
|
|
268
329
|
const done = [];
|
|
269
330
|
const cleaned = runRecipeCleanup(plan.cleanup, { appRoot, platform, log, spawnSync, ...(fs ? { fs } : {}) });
|
|
270
331
|
let stable = [];
|
|
332
|
+
let staged = [];
|
|
333
|
+
const stagePost = plan.stage?.post ?? [];
|
|
334
|
+
const resolveEntry = (entry, appKey) => ({ ...entry, [appKey]: path.resolve(appRoot, entry[appKey]) });
|
|
271
335
|
for (const [index, step] of plan.steps.entries()) {
|
|
336
|
+
const verifyReal = (what) => { for (const entry of plan.stage?.verify ?? []) verifySameContent({ app: path.resolve(appRoot, entry.app), real: entry.real }, { what, ...(fs ? { fs } : {}) }); };
|
|
337
|
+
const checkDir = (what) => { if (plan.stage?.resolve) assertTargetDirUnchanged(plan.stage.resolve, resolveTargetRoot, { what }); };
|
|
338
|
+
if (step.phase === "build" && plan.stage) {
|
|
339
|
+
checkDir("before the build step");
|
|
340
|
+
removeFreshSources(stagePost, { ...(fs ? { fs } : {}), log });
|
|
341
|
+
staged = stageArtifacts((plan.stage.pre ?? []).map((entry) => resolveEntry(entry, "from")), { ...(fs ? { fs } : {}), log, requireParent: true });
|
|
342
|
+
// Immediately before the step that embeds the real EXE: it must be exactly the signed in-app copy.
|
|
343
|
+
verifyReal("after the copy-back, before the build step");
|
|
344
|
+
}
|
|
272
345
|
const launch = resolveLaunch({ cmd: step.cmd, args: step.args, platform, env });
|
|
273
346
|
const cwd = path.resolve(appRoot, step.cwd ?? ".");
|
|
274
347
|
log(`recipe-build: [${step.phase}] ${step.cmd} ${step.args.join(" ")}`.trimEnd());
|
|
@@ -277,9 +350,13 @@ export function runRecipeBuild(rawPlan, { appRoot, env = process.env, platform =
|
|
|
277
350
|
fail("RECIPE_BUILD_STEP_FAILED", `recipe-build ${step.phase} step ${index} (${step.cmd}) failed (${result?.error?.message ?? `exit ${result?.status ?? result?.signal ?? "unknown"}`}); later steps were not run`, { phase: step.phase, index, cmd: step.cmd, status: result?.status ?? null });
|
|
278
351
|
}
|
|
279
352
|
done.push({ phase: step.phase, cmd: step.cmd, status: 0 });
|
|
353
|
+
if (step.phase === "build" && plan.stage) { verifyReal("after the build step"); checkDir("after the build step"); }
|
|
354
|
+
// The REAL outputs must stop changing before anything is copied: a half-written EXE is never staged.
|
|
355
|
+
if (step.phase === "build" && stagePost.length) waitForStableArtifacts({ ...(plan.stability ?? {}), files: stagePost.map((entry) => entry.from).filter((file) => (fs?.existsSync ?? nodeExistsSync)(file)) }, { appRoot, log, ...(sleep ? { sleep } : {}), ...(now ? { now } : {}), ...(statSync ? { statSync } : {}) });
|
|
356
|
+
if (step.phase === "build" && stagePost.length) staged.push(...stageArtifacts(stagePost.map((entry) => resolveEntry(entry, "to")), { ...(fs ? { fs } : {}), log }));
|
|
280
357
|
if (step.phase === "build") stable = waitForStableArtifacts(plan.stability, { appRoot, log, ...(sleep ? { sleep } : {}), ...(now ? { now } : {}), ...(statSync ? { statSync } : {}) });
|
|
281
358
|
}
|
|
282
|
-
return { steps: done, cleaned, stable };
|
|
359
|
+
return { steps: done, cleaned, stable, staged };
|
|
283
360
|
}
|
|
284
361
|
|
|
285
362
|
export function parseRecipeBuildArgs(argv) {
|
package/release-switch-scan.mjs
CHANGED
|
@@ -48,6 +48,8 @@ import path from "node:path";
|
|
|
48
48
|
import { spawnSync } from "./background-process.mjs";
|
|
49
49
|
import { featuresInTokens, resolveCargoManifestPath, scriptTarget, tokenize } from "./qa-build-features.mjs";
|
|
50
50
|
import { isNativeBinaryHeader } from "./qa-control-scan.mjs";
|
|
51
|
+
import { nodeHostTriple } from "./stage-sidecar-command.mjs";
|
|
52
|
+
import { WIN_PACKAGE_DEFAULT_TRIPLE } from "./win-package-recipe.mjs";
|
|
51
53
|
|
|
52
54
|
export const TEST_MODE_MARKER = "RIGHTKIT-TEST-MODE-COMPILED"; // identical to `TEST_MODE_MARKER_TEXT` in rightkit-daemon/src/mode.rs
|
|
53
55
|
const CHUNK = 4 * 1024 * 1024;
|
|
@@ -164,8 +166,45 @@ export function resolveFeatureClosure(options = {}) {
|
|
|
164
166
|
return resolveCargoTree(options);
|
|
165
167
|
}
|
|
166
168
|
|
|
169
|
+
const MAC_SLICES = ["aarch64-apple-darwin", "x86_64-apple-darwin"];
|
|
170
|
+
const UPDATER_PLATFORM_TRIPLES = {
|
|
171
|
+
"darwin-aarch64": ["mac", "aarch64-apple-darwin"],
|
|
172
|
+
"darwin-x86_64": ["mac", "x86_64-apple-darwin"],
|
|
173
|
+
"windows-x86_64": ["win", "x86_64-pc-windows-msvc"],
|
|
174
|
+
"windows-aarch64": ["win", "aarch64-pc-windows-msvc"],
|
|
175
|
+
};
|
|
176
|
+
const PLATFORM_HOST_OS = { win: "windows", mac: "apple-darwin" };
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The Rust triples a release of `target` ships, evaluated on the RAW config target (before recipe expansion), so the
|
|
180
|
+
* feature closure checks exactly the slices that ship and never asks cargo for `--target all` (the RustRight broker
|
|
181
|
+
* refuses it). First matching rule wins. Throws `release-switch-target-unknown` when no shipped triple can be named.
|
|
182
|
+
*/
|
|
183
|
+
export function shippedRustTriples({ platform, target, hostTriple = nodeHostTriple } = {}) {
|
|
184
|
+
const explicit = target?.cargoTarget ?? target?.targetTriple ?? target?.rustTarget;
|
|
185
|
+
if (explicit) return explicit === "universal-apple-darwin" ? [...MAC_SLICES] : [explicit];
|
|
186
|
+
if (target?.winPackage) return [target.winPackage.triple ?? WIN_PACKAGE_DEFAULT_TRIPLE];
|
|
187
|
+
if (target?.macPackage) {
|
|
188
|
+
const spec = target.macPackage;
|
|
189
|
+
if (spec.triple) return spec.triple === "universal-apple-darwin" ? [...MAC_SLICES] : [spec.triple];
|
|
190
|
+
if (spec.universal === false) return [hostTriple()];
|
|
191
|
+
return [...MAC_SLICES];
|
|
192
|
+
}
|
|
193
|
+
if (platform === "ios") return ["aarch64-apple-ios"];
|
|
194
|
+
if (platform === "win" || platform === "mac") {
|
|
195
|
+
const fromUpdater = [...new Set((target?.updater?.artifacts ?? [])
|
|
196
|
+
.map((artifact) => UPDATER_PLATFORM_TRIPLES[artifact?.platform])
|
|
197
|
+
.filter((entry) => entry && entry[0] === platform)
|
|
198
|
+
.map((entry) => entry[1]))];
|
|
199
|
+
if (fromUpdater.length) return fromUpdater;
|
|
200
|
+
const host = hostTriple();
|
|
201
|
+
if (host.includes(PLATFORM_HOST_OS[platform])) return [host];
|
|
202
|
+
}
|
|
203
|
+
throw new Error(`release-switch-target-unknown: cannot name the Rust triple(s) the ${platform} release ships; declare targets.${platform}.cargoTarget`);
|
|
204
|
+
}
|
|
205
|
+
|
|
167
206
|
/** Shared Cargo tree resolver; keeps each build command's feature selection separate. */
|
|
168
|
-
export function resolveCargoTree({ root, manifest, cargoManifest, rust, features = [], scripts, triple, commandTexts = [],
|
|
207
|
+
export function resolveCargoTree({ root, manifest, cargoManifest, rust, features = [], scripts, triple, triples, commandTexts = [],
|
|
169
208
|
edges = "features,no-dev", prefix, noDefaultFeatures = false, label = "feature closure", run = spawnSync } = {}) {
|
|
170
209
|
const { rel, declared, candidates } = resolveCargoManifestPath({ root, cargoManifest, manifest });
|
|
171
210
|
if (!rel) {
|
|
@@ -175,8 +214,11 @@ export function resolveCargoTree({ root, manifest, cargoManifest, rust, features
|
|
|
175
214
|
if (declared.length && !declared.some((c) => existsSync(path.join(root, c)))) {
|
|
176
215
|
throw new Error(`release-switch-manifest-missing: declared manifest ${declared.join(", ")} does not exist under ${root}`);
|
|
177
216
|
}
|
|
178
|
-
const baseArgs = ["tree", "--color", "never", "-e", edges, "--locked", "--manifest-path", path.join(root, rel)
|
|
217
|
+
const baseArgs = ["tree", "--color", "never", "-e", edges, "--locked", "--manifest-path", path.join(root, rel)];
|
|
179
218
|
if (prefix) baseArgs.push("--prefix", prefix);
|
|
219
|
+
// `triples` may be a function so a target that cannot name its triples still skips cleanly when the app declares no Rust.
|
|
220
|
+
const tripleList = [...new Set((typeof triples === "function" ? triples() : triples) ?? (triple ? [triple] : []))];
|
|
221
|
+
if (tripleList.length === 0) throw new Error(`${label}: no Rust target triple given; the closure never runs \`cargo tree --target all\` (the build broker refuses it). Declare targets.<os>.cargoTarget`);
|
|
180
222
|
const pkgScripts = scripts ?? readPackageScripts(root);
|
|
181
223
|
// One cargo tree per distinct (features, no-default-features, manifest) combination, one combination per build command
|
|
182
224
|
// (flags gathered over the command and the package.json scripts it runs). A `--no-default-features` in one command never
|
|
@@ -207,7 +249,8 @@ export function resolveCargoTree({ root, manifest, cargoManifest, rust, features
|
|
|
207
249
|
for (const text of texts) comboFor(text);
|
|
208
250
|
const trees = [];
|
|
209
251
|
for (const { noDefault, allFeatures, list } of combos.values()) {
|
|
210
|
-
const
|
|
252
|
+
for (const tripleArg of tripleList) {
|
|
253
|
+
const args = [...baseArgs, "--target", tripleArg];
|
|
211
254
|
if (noDefault) args.push("--no-default-features");
|
|
212
255
|
if (allFeatures) args.push("--all-features");
|
|
213
256
|
if (list.length) args.push("--features", list.join(","));
|
|
@@ -218,6 +261,7 @@ export function resolveCargoTree({ root, manifest, cargoManifest, rust, features
|
|
|
218
261
|
const out = String(result.stdout ?? "");
|
|
219
262
|
if (!out.trim()) throw new Error(`${label}: \`cargo tree\` printed nothing; cannot verify`);
|
|
220
263
|
trees.push(out);
|
|
264
|
+
}
|
|
221
265
|
}
|
|
222
266
|
const treeText = trees.join("\n");
|
|
223
267
|
return { treeText, trees, manifest: rel };
|
package/release.mjs
CHANGED
|
@@ -49,7 +49,7 @@ import { assertNoQaControl, assertNoWebBundleMocks } from "./qa-control-scan.mjs
|
|
|
49
49
|
import { assertReleaseDependencies } from "./release-dependencies.mjs";
|
|
50
50
|
import { assertNoForbiddenBuildFeatures } from "./qa-build-features.mjs";
|
|
51
51
|
import { expandRecipeBuildCommands } from "./recipe-build.mjs";
|
|
52
|
-
import { assertNoReleaseSwitches, commandTokens, localCrateDirsFromTree, resolveFeatureClosure } from "./release-switch-scan.mjs";
|
|
52
|
+
import { assertNoReleaseSwitches, commandTokens, localCrateDirsFromTree, resolveFeatureClosure, shippedRustTriples } from "./release-switch-scan.mjs";
|
|
53
53
|
import { iosOwnershipScope } from "./ownership-scope.mjs";
|
|
54
54
|
import { devtoolsEvidence, resolveDevtoolsAllowance } from "./devtools-policy.mjs";
|
|
55
55
|
import { runAndroidRelease, validateAndroidTarget } from "./android-release.mjs";
|
|
@@ -472,8 +472,8 @@ try {
|
|
|
472
472
|
cargoManifest: switchInputs.cargoManifest,
|
|
473
473
|
rust: config.releaseSwitches?.rust,
|
|
474
474
|
features: config.releaseSwitches?.features ?? [],
|
|
475
|
-
//
|
|
476
|
-
|
|
475
|
+
// The shipped triples (never "all": the build broker refuses it); evaluated on the raw config target.
|
|
476
|
+
triples: () => shippedRustTriples({ platform: opts.platform, target: config.targets?.[opts.platform] }),
|
|
477
477
|
commandTexts: closureCommands,
|
|
478
478
|
});
|
|
479
479
|
if (closure.skipped) console.log(`right-release: feature closure skipped (${closure.skipped})`);
|
package/rightkit-versions.json
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"@rightkit/hooks": "0.1.2",
|
|
21
21
|
"@rightkit/i18n": "0.2.0",
|
|
22
22
|
"@rightkit/issues-tracker": "0.1.0",
|
|
23
|
-
"@rightkit/legal": "0.6.
|
|
23
|
+
"@rightkit/legal": "0.6.3",
|
|
24
24
|
"@rightkit/legal-ui": "0.4.1",
|
|
25
25
|
"@rightkit/license": "0.3.0",
|
|
26
26
|
"@rightkit/logs": "0.1.6",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"@rightkit/qa": "0.6.1",
|
|
33
33
|
"@rightkit/qa-chrome": "0.1.11",
|
|
34
34
|
"@rightkit/react-shell": "0.7.2",
|
|
35
|
-
"@rightkit/release": "0.6.
|
|
35
|
+
"@rightkit/release": "0.6.2",
|
|
36
36
|
"@rightkit/settings-ui": "0.7.0",
|
|
37
37
|
"@rightkit/tauri": "0.3.0",
|
|
38
38
|
"@rightkit/tauri-shell": "0.1.2",
|
|
@@ -107,7 +107,8 @@
|
|
|
107
107
|
"0.3.1",
|
|
108
108
|
"0.5.0",
|
|
109
109
|
"0.6.0",
|
|
110
|
-
"0.6.1"
|
|
110
|
+
"0.6.1",
|
|
111
|
+
"0.6.2"
|
|
111
112
|
],
|
|
112
113
|
"@rightkit/license": [
|
|
113
114
|
"0.1.6",
|
|
@@ -226,7 +227,8 @@
|
|
|
226
227
|
"0.4.1",
|
|
227
228
|
"0.5.0",
|
|
228
229
|
"0.5.1",
|
|
229
|
-
"0.6.0"
|
|
230
|
+
"0.6.0",
|
|
231
|
+
"0.6.1"
|
|
230
232
|
],
|
|
231
233
|
"@rightkit/qa": [
|
|
232
234
|
"0.1.0",
|
|
@@ -464,7 +466,7 @@
|
|
|
464
466
|
"rightkit-identity": "0.1.0",
|
|
465
467
|
"rightkit-job-queue": "0.5.0",
|
|
466
468
|
"rightkit-license": "0.2.4",
|
|
467
|
-
"rightkit-llm": "0.3.
|
|
469
|
+
"rightkit-llm": "0.3.1",
|
|
468
470
|
"rightkit-logs": "0.3.5",
|
|
469
471
|
"rightkit-mcp": "0.7.0",
|
|
470
472
|
"rightkit-models": "0.5.1",
|
|
@@ -550,7 +552,7 @@
|
|
|
550
552
|
"rightkit-job-queue": "0.5.0",
|
|
551
553
|
"rightkit-jobs": "0.3.0",
|
|
552
554
|
"rightkit-license": "0.2.4",
|
|
553
|
-
"rightkit-llm": "0.3.
|
|
555
|
+
"rightkit-llm": "0.3.1",
|
|
554
556
|
"rightkit-logs": "0.3.5",
|
|
555
557
|
"rightkit-mcp": "0.7.0",
|
|
556
558
|
"rightkit-media": "0.2.6",
|
|
@@ -608,7 +610,7 @@
|
|
|
608
610
|
"@rightkit/hooks": "0.1.2",
|
|
609
611
|
"@rightkit/i18n": "0.2.0",
|
|
610
612
|
"@rightkit/issues-tracker": "0.1.0",
|
|
611
|
-
"@rightkit/legal": "0.6.
|
|
613
|
+
"@rightkit/legal": "0.6.3",
|
|
612
614
|
"@rightkit/legal-ui": "0.4.1",
|
|
613
615
|
"@rightkit/license": "0.3.0",
|
|
614
616
|
"@rightkit/logs": "0.1.6",
|
|
@@ -620,7 +622,7 @@
|
|
|
620
622
|
"@rightkit/qa": "0.6.1",
|
|
621
623
|
"@rightkit/qa-chrome": "0.1.11",
|
|
622
624
|
"@rightkit/react-shell": "0.7.2",
|
|
623
|
-
"@rightkit/release": "0.6.
|
|
625
|
+
"@rightkit/release": "0.6.1",
|
|
624
626
|
"@rightkit/settings-ui": "0.7.0",
|
|
625
627
|
"@rightkit/shell": "0.1.1",
|
|
626
628
|
"@rightkit/tauri": "0.3.0",
|