@rightkit/release 0.2.172 → 0.2.174
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 +10 -4
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/darwin/rightkit-swap.sign.json +1 -1
- package/cli/right-release.mjs +7 -3
- package/development-signing.mjs +135 -4
- package/development.mjs +27 -12
- package/in-place-install.mjs +32 -7
- package/install-app-windows.mjs +108 -17
- package/install-app.mjs +32 -8
- package/install-tauri-devid.mjs +231 -0
- package/inventory-check.mjs +1 -1
- package/mac-bundle-signing.mjs +143 -1
- package/mac-package-recipe.mjs +304 -0
- package/mac-signing-matrix.mjs +73 -6
- package/macos-signing-session.mjs +12 -2
- package/media-sidecar.mjs +1 -1
- package/package-recipe-targets.mjs +245 -0
- package/package.json +1 -1
- package/portable-development.mjs +9 -9
- package/project-type.mjs +1 -1
- package/qa-build-features.mjs +7 -1
- package/recipe-build.mjs +346 -0
- package/release.mjs +27 -9
- package/rightkit-signing-agent.mjs +10 -4
- package/rightkit-sync.mjs +1 -1
- package/rightkit-versions.json +11 -6
- package/runtime-budgets.mjs +14 -5
- package/stage-binary.mjs +1 -1
- package/stage-sidecar-command.mjs +73 -0
- package/stage-sidecar.mjs +839 -0
- package/swift-mac-dmg.mjs +2 -1
- package/win-package-recipe.mjs +321 -0
package/recipe-build.mjs
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `right-release recipe-build --plan <json>` (bogusyogi/rightkit#345): the single prePackage command that a declarative
|
|
3
|
+
// winPackage / macPackage target expands to. release.mjs runs one prePackage command; a real Tauri app needs several
|
|
4
|
+
// steps before and after `tauri build` (stage ffmpeg / pdfium / sidecars, then the build, then for macOS embed a dylib
|
|
5
|
+
// before release.mjs signs). This runs them in order from a validated plan, with argv arrays and no shell, and fails
|
|
6
|
+
// closed on the first step that does not exit 0. It never signs; release.mjs signs what it leaves behind.
|
|
7
|
+
//
|
|
8
|
+
// Plan: { version: 1, steps: [{ phase: "prepare" | "build" | "postBuild", cmd, args?, cwd?, env? }], cleanup?, stability? }
|
|
9
|
+
// `cwd` is relative to the app root (default the app root). Phases must appear in order prepare, build, postBuild, and
|
|
10
|
+
// exactly one build step is required.
|
|
11
|
+
//
|
|
12
|
+
// Built-in steps (no app script needed), all paths relative to the app root:
|
|
13
|
+
// cleanup: { remove?: [file or dir], apps?: { dir, remove?: bool, unregister?: bool }, dmgVolumes?: [name prefix] }
|
|
14
|
+
// runs BEFORE the first step. `remove` deletes stale artifacts (a stale raw EXE or installer must never be
|
|
15
|
+
// mistaken for this build's output). `apps` finds stale `*.app` bundles in `dir`, unregisters them with
|
|
16
|
+
// `lsregister -u` and/or deletes them. `dmgVolumes` detaches /Volumes/<name> and /Volumes/<name> <N> (the macOS
|
|
17
|
+
// duplicate suffix) only when `hdiutil info -plist` shows the volume is a mounted disk image; `-force` is used
|
|
18
|
+
// only on retry. The macOS-only parts (apps unregister, dmgVolumes) are skipped off darwin.
|
|
19
|
+
// stability: { files: [file], intervalMs?, checks?, timeoutMs? } runs right AFTER the build step: each file must exist
|
|
20
|
+
// and keep the same size and mtime over `checks` consecutive polls `intervalMs` apart (defaults 2 and 1000 ms,
|
|
21
|
+
// timeout 120 s), so a hung or still-writing Tauri process cannot hand a half-written EXE to signing.
|
|
22
|
+
import { existsSync as nodeExistsSync, readFileSync, readdirSync as nodeReaddirSync, rmSync as nodeRmSync, statSync as nodeStatSync } from "node:fs";
|
|
23
|
+
import path from "node:path";
|
|
24
|
+
import { pathToFileURL } from "node:url";
|
|
25
|
+
import { spawnSync as nodeSpawnSync } from "./background-process.mjs";
|
|
26
|
+
|
|
27
|
+
export const RECIPE_BUILD_PLAN_VERSION = 1;
|
|
28
|
+
export const RECIPE_PHASES = Object.freeze(["prepare", "build", "postBuild"]);
|
|
29
|
+
|
|
30
|
+
export class RecipeBuildError extends Error {
|
|
31
|
+
constructor(code, message, details = {}) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = "RecipeBuildError";
|
|
34
|
+
this.code = code;
|
|
35
|
+
this.details = details;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const fail = (code, message, details) => { throw new RecipeBuildError(code, message, details); };
|
|
40
|
+
const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
41
|
+
const STEP_KEYS = new Set(["phase", "cmd", "args", "cwd", "env"]);
|
|
42
|
+
const PLAN_KEYS = new Set(["version", "steps", "cleanup", "stability"]);
|
|
43
|
+
const CLEANUP_KEYS = new Set(["remove", "apps", "dmgVolumes"]);
|
|
44
|
+
const APPS_KEYS = new Set(["dir", "remove", "unregister"]);
|
|
45
|
+
const STABILITY_KEYS = new Set(["files", "intervalMs", "checks", "timeoutMs"]);
|
|
46
|
+
const LSREGISTER = "/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister";
|
|
47
|
+
const unknownKeys = (value, allowed, label, errors) => { for (const key of Object.keys(value)) if (!allowed.has(key)) errors.push(`${label}: unknown key ${JSON.stringify(key)} (allowed: ${[...allowed].join(", ")})`); };
|
|
48
|
+
const positiveInt = (value) => Number.isInteger(value) && value > 0;
|
|
49
|
+
const badVolumePrefix = (item) => typeof item !== "string" || !item.trim() || item === "." || item === ".." || item.includes("/") || item.includes("\\") || item.includes(":") || item.includes("\0");
|
|
50
|
+
|
|
51
|
+
/** A relative path that stays inside the app root; returns the normalized form or undefined and pushes an error. */
|
|
52
|
+
function insideApp(value, label, errors, { strict = false } = {}) {
|
|
53
|
+
if (typeof value !== "string" || !value.trim()) { errors.push(`${label} must be a non-empty relative path`); return undefined; }
|
|
54
|
+
const normalized = value.replaceAll("\\", "/");
|
|
55
|
+
if (path.isAbsolute(value) || /^[A-Za-z]:/.test(value) || normalized.split("/").includes("..")) { errors.push(`${label} must be relative and stay inside the app: ${value}`); return undefined; }
|
|
56
|
+
const resolved = path.posix.normalize(normalized);
|
|
57
|
+
if (strict && (resolved === "." || resolved === "./" || resolved === "")) { errors.push(`${label} must name a path strictly inside the app, not the app root: ${JSON.stringify(value)}`); return undefined; }
|
|
58
|
+
if (resolved === "/" || resolved.startsWith("../") || resolved === "..") { errors.push(`${label} must name a path strictly inside the app, not the app root or an ancestor: ${JSON.stringify(value)}`); return undefined; }
|
|
59
|
+
return normalized;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function validateCleanup(value, errors) {
|
|
63
|
+
if (value === undefined) return undefined;
|
|
64
|
+
if (!isObject(value)) { errors.push("cleanup must be an object"); return undefined; }
|
|
65
|
+
unknownKeys(value, CLEANUP_KEYS, "cleanup", errors);
|
|
66
|
+
const out = {};
|
|
67
|
+
if (value.remove !== undefined) {
|
|
68
|
+
if (!Array.isArray(value.remove)) errors.push("cleanup.remove must be a list of relative paths");
|
|
69
|
+
else out.remove = value.remove.map((item, index) => insideApp(item, `cleanup.remove[${index}]`, errors, { strict: true })).filter(Boolean);
|
|
70
|
+
}
|
|
71
|
+
if (value.apps !== undefined) {
|
|
72
|
+
if (!isObject(value.apps)) errors.push("cleanup.apps must be an object");
|
|
73
|
+
else {
|
|
74
|
+
unknownKeys(value.apps, APPS_KEYS, "cleanup.apps", errors);
|
|
75
|
+
for (const key of ["remove", "unregister"]) if (value.apps[key] !== undefined && typeof value.apps[key] !== "boolean") errors.push(`cleanup.apps.${key} must be a boolean`);
|
|
76
|
+
const dir = insideApp(value.apps.dir, "cleanup.apps.dir", errors, { strict: true });
|
|
77
|
+
if (dir) out.apps = { dir, remove: value.apps.remove === true, unregister: value.apps.unregister === true };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (value.dmgVolumes !== undefined) {
|
|
81
|
+
if (!Array.isArray(value.dmgVolumes) || value.dmgVolumes.some(badVolumePrefix)) errors.push("cleanup.dmgVolumes must be a list of non-empty volume name prefixes (no path separators)");
|
|
82
|
+
else out.dmgVolumes = [...value.dmgVolumes];
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function validateStability(value, errors) {
|
|
88
|
+
if (value === undefined) return undefined;
|
|
89
|
+
if (!isObject(value)) { errors.push("stability must be an object"); return undefined; }
|
|
90
|
+
unknownKeys(value, STABILITY_KEYS, "stability", errors);
|
|
91
|
+
for (const key of ["intervalMs", "checks", "timeoutMs"]) if (value[key] !== undefined && !positiveInt(value[key])) errors.push(`stability.${key} must be a positive integer`);
|
|
92
|
+
if (!Array.isArray(value.files) || !value.files.length) { errors.push("stability.files must be a non-empty list of relative paths"); return undefined; }
|
|
93
|
+
const files = value.files.map((item, index) => insideApp(item, `stability.files[${index}]`, errors, { strict: true })).filter(Boolean);
|
|
94
|
+
return { files, ...(value.intervalMs === undefined ? {} : { intervalMs: value.intervalMs }), ...(value.checks === undefined ? {} : { checks: value.checks }), ...(value.timeoutMs === undefined ? {} : { timeoutMs: value.timeoutMs }) };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Validate a plan strictly; every problem is reported at once as RECIPE_BUILD_PLAN_INVALID. Returns the normalized plan. */
|
|
98
|
+
export function validateRecipePlan(plan) {
|
|
99
|
+
const errors = [];
|
|
100
|
+
if (!isObject(plan)) fail("RECIPE_BUILD_PLAN_INVALID", "recipe plan must be an object");
|
|
101
|
+
for (const key of Object.keys(plan)) if (!PLAN_KEYS.has(key)) errors.push(`unknown key ${JSON.stringify(key)}`);
|
|
102
|
+
if (plan.version !== RECIPE_BUILD_PLAN_VERSION) errors.push(`version must be ${RECIPE_BUILD_PLAN_VERSION}`);
|
|
103
|
+
if (!Array.isArray(plan.steps) || !plan.steps.length) errors.push("steps must be a non-empty array");
|
|
104
|
+
const steps = [];
|
|
105
|
+
let lastPhase = -1;
|
|
106
|
+
let builds = 0;
|
|
107
|
+
for (const [index, step] of (Array.isArray(plan.steps) ? plan.steps : []).entries()) {
|
|
108
|
+
const label = `steps[${index}]`;
|
|
109
|
+
if (!isObject(step)) { errors.push(`${label} must be an object`); continue; }
|
|
110
|
+
for (const key of Object.keys(step)) if (!STEP_KEYS.has(key)) errors.push(`${label}: unknown key ${JSON.stringify(key)} (allowed: ${[...STEP_KEYS].join(", ")})`);
|
|
111
|
+
const phase = RECIPE_PHASES.indexOf(step.phase);
|
|
112
|
+
if (phase < 0) errors.push(`${label}.phase must be one of ${RECIPE_PHASES.join(", ")}`);
|
|
113
|
+
else if (phase < lastPhase) errors.push(`${label}: phase ${step.phase} comes after a later phase (order: ${RECIPE_PHASES.join(", ")})`);
|
|
114
|
+
else lastPhase = phase;
|
|
115
|
+
if (step.phase === "build") builds += 1;
|
|
116
|
+
if (typeof step.cmd !== "string" || !step.cmd.trim()) errors.push(`${label}.cmd must be a non-empty string`);
|
|
117
|
+
const args = step.args ?? [];
|
|
118
|
+
if (!Array.isArray(args) || args.some((arg) => typeof arg !== "string")) errors.push(`${label}.args must be a list of strings`);
|
|
119
|
+
const cwd = step.cwd === undefined ? undefined : insideApp(step.cwd, `${label}.cwd`, errors);
|
|
120
|
+
if (step.env !== undefined && (!isObject(step.env) || Object.values(step.env).some((value) => typeof value !== "string"))) errors.push(`${label}.env must map names to strings`);
|
|
121
|
+
steps.push({ phase: step.phase, cmd: step.cmd, args: Array.isArray(args) ? [...args] : [], ...(cwd ? { cwd } : {}), ...(isObject(step.env) ? { env: { ...step.env } } : {}) });
|
|
122
|
+
}
|
|
123
|
+
if (builds !== 1) errors.push(`exactly one build step is required, found ${builds}`);
|
|
124
|
+
const cleanup = validateCleanup(plan.cleanup, errors);
|
|
125
|
+
const stability = validateStability(plan.stability, errors);
|
|
126
|
+
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 } : {}) };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// cmd.exe would reinterpret these in a shell launch; refuse rather than escape.
|
|
131
|
+
const SHELL_UNSAFE = /[&|<>^%"!\r\n]/;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* How to launch one step without a shell. `pnpm` runs through the pinned pnpm CLI (npm_execpath, set whenever the
|
|
135
|
+
* release runs under pnpm). On Windows a non-.exe command is a .cmd shim that only a shell can start, so it is allowed
|
|
136
|
+
* only when no argument could be reinterpreted by that shell.
|
|
137
|
+
*/
|
|
138
|
+
export function resolveLaunch({ cmd, args = [], platform = process.platform, env = process.env, execPath = process.execPath }) {
|
|
139
|
+
if (cmd === "pnpm" && env.npm_execpath) return { command: execPath, args: [env.npm_execpath, ...args], shell: false };
|
|
140
|
+
if (platform === "win32" && !/\.(?:exe|com)$/i.test(cmd)) {
|
|
141
|
+
if ([cmd, ...args].some((value) => SHELL_UNSAFE.test(value))) fail("RECIPE_BUILD_LAUNCH_UNSAFE", `${cmd} needs a shell on Windows and its arguments contain characters a shell would reinterpret; run the release under pnpm (npm_execpath) or name an .exe`, { cmd });
|
|
142
|
+
return { command: cmd, args, shell: true };
|
|
143
|
+
}
|
|
144
|
+
return { command: cmd, args, shell: false };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const defaultSleep = (ms) => { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); };
|
|
148
|
+
const defaultFs = { existsSync: nodeExistsSync, readdirSync: nodeReaddirSync, rmSync: nodeRmSync };
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Mount points of mounted disk images in `hdiutil info -plist` output: a mount-point counts only when it follows an
|
|
152
|
+
* `image-path` key (so it belongs to an image). Anything unparsable yields an empty set, so nothing is detached.
|
|
153
|
+
*/
|
|
154
|
+
export function diskImageMountPoints(plistText) {
|
|
155
|
+
const mounts = new Set();
|
|
156
|
+
const segments = String(plistText).split("<key>image-path</key>").slice(1);
|
|
157
|
+
for (const segment of segments) {
|
|
158
|
+
for (const match of segment.matchAll(/<key>mount-point<\/key>\s*<string>([^<]*)<\/string>/g)) mounts.add(match[1].replaceAll("&", "&"));
|
|
159
|
+
}
|
|
160
|
+
return mounts;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Stale-artifact cleanup, run before the build. Returns what it did: { removed, unregistered, detached }.
|
|
165
|
+
* A failing detach or unregister is logged and skipped: the build's own gates decide pass or fail.
|
|
166
|
+
* Options: appRoot, platform, log, spawnSync, fs ({ existsSync, readdirSync, rmSync }), volumesDir.
|
|
167
|
+
*/
|
|
168
|
+
export function runRecipeCleanup(cleanup, { appRoot, platform = process.platform, log = () => {}, spawnSync = nodeSpawnSync, fs = defaultFs, volumesDir = "/Volumes" } = {}) {
|
|
169
|
+
const result = { removed: [], unregistered: [], detached: [] };
|
|
170
|
+
if (!cleanup) return result;
|
|
171
|
+
const tool = (cmd, args) => {
|
|
172
|
+
const run = spawnSync(cmd, args, { stdio: "inherit", windowsHide: true });
|
|
173
|
+
const ok = !(run?.error || run?.status !== 0);
|
|
174
|
+
if (!ok) log(`recipe-build: cleanup ${cmd} ${args.join(" ")} did not succeed (${run?.error?.message ?? `exit ${run?.status ?? "unknown"}`}); continuing`);
|
|
175
|
+
return ok;
|
|
176
|
+
};
|
|
177
|
+
if (cleanup.dmgVolumes?.length && platform !== "darwin") log("recipe-build: cleanup dmgVolumes skipped (not macOS)");
|
|
178
|
+
else {
|
|
179
|
+
// Only a volume named exactly `<name>` or `<name> <N>` (the macOS duplicate suffix) that `hdiutil info -plist` reports as
|
|
180
|
+
// a mounted disk image is detached; a real disk, a network share or a similarly named folder is never touched.
|
|
181
|
+
const candidatesFor = (names) => {
|
|
182
|
+
const wanted = cleanup.dmgVolumes ?? [];
|
|
183
|
+
return names.filter((entry) => wanted.some((volName) => entry === volName || (entry.startsWith(`${volName} `) && /^[0-9]+$/.test(entry.slice(volName.length + 1))))).sort();
|
|
184
|
+
};
|
|
185
|
+
let names = [];
|
|
186
|
+
try { names = fs.readdirSync(volumesDir); } catch { names = []; }
|
|
187
|
+
const candidates = candidatesFor(names);
|
|
188
|
+
if (candidates.length) {
|
|
189
|
+
const info = spawnSync("hdiutil", ["info", "-plist"], { encoding: "utf8", windowsHide: true });
|
|
190
|
+
const imageMounts = info?.error || info?.status !== 0 ? null : diskImageMountPoints(String(info.stdout ?? ""));
|
|
191
|
+
if (!imageMounts) log("recipe-build: cleanup dmgVolumes skipped (hdiutil info -plist failed, so no volume is confirmed to be a disk image)");
|
|
192
|
+
else {
|
|
193
|
+
for (const name of candidates) {
|
|
194
|
+
const volume = path.posix.join(volumesDir, name);
|
|
195
|
+
if (!imageMounts.has(volume)) { log(`recipe-build: cleanup leaves ${volume} (not a mounted disk image)`); continue; }
|
|
196
|
+
log(`recipe-build: cleanup detach ${volume}`);
|
|
197
|
+
// No -force first; -force only on retry, and only for a volume confirmed to be an image mount.
|
|
198
|
+
if (tool("hdiutil", ["detach", volume]) || tool("hdiutil", ["detach", "-force", volume])) result.detached.push(name);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (cleanup.apps) {
|
|
204
|
+
const dir = path.resolve(appRoot, cleanup.apps.dir);
|
|
205
|
+
let entries = [];
|
|
206
|
+
try { entries = fs.readdirSync(dir).filter((entry) => entry.toLowerCase().endsWith(".app")).sort(); } catch { entries = []; }
|
|
207
|
+
for (const entry of entries) {
|
|
208
|
+
const stale = path.join(dir, entry);
|
|
209
|
+
const root = path.resolve(appRoot);
|
|
210
|
+
const rel = path.relative(stale, root);
|
|
211
|
+
if (rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel)) || (dir === root && entry.toLowerCase() === "notarize.app")) {
|
|
212
|
+
fail("RECIPE_BUILD_CLEANUP_UNSAFE", `cleanup refuses ${stale}: it is, or contains, the app root ${root}`);
|
|
213
|
+
}
|
|
214
|
+
if (cleanup.apps.unregister) {
|
|
215
|
+
if (platform !== "darwin") log("recipe-build: cleanup lsregister skipped (not macOS)");
|
|
216
|
+
else if (tool(LSREGISTER, ["-u", stale])) result.unregistered.push(stale);
|
|
217
|
+
}
|
|
218
|
+
if (cleanup.apps.remove) {
|
|
219
|
+
log(`recipe-build: cleanup remove stale app ${stale}`);
|
|
220
|
+
fs.rmSync(stale, { recursive: true, force: true });
|
|
221
|
+
result.removed.push(stale);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
for (const relative of cleanup.remove ?? []) {
|
|
226
|
+
const stale = path.resolve(appRoot, relative);
|
|
227
|
+
if (!fs.existsSync(stale)) continue;
|
|
228
|
+
log(`recipe-build: cleanup remove stale ${stale}`);
|
|
229
|
+
fs.rmSync(stale, { recursive: true, force: true });
|
|
230
|
+
result.removed.push(stale);
|
|
231
|
+
}
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Wait until every file exists and keeps the same size and mtime over `checks` consecutive polls `intervalMs` apart.
|
|
237
|
+
* Throws RECIPE_BUILD_ARTIFACT_UNSTABLE when that does not happen within `timeoutMs`. Returns the absolute files.
|
|
238
|
+
* Options: appRoot, log, statSync, sleep(ms), now().
|
|
239
|
+
*/
|
|
240
|
+
export function waitForStableArtifacts(stability, { appRoot, log = () => {}, statSync = nodeStatSync, sleep = defaultSleep, now = Date.now } = {}) {
|
|
241
|
+
if (!stability) return [];
|
|
242
|
+
const { intervalMs = 1000, checks = 2, timeoutMs = 120000 } = stability;
|
|
243
|
+
const signature = (file) => { try { const stat = statSync(file); return `${stat.size}:${stat.mtimeMs}`; } catch { return null; } };
|
|
244
|
+
const files = stability.files.map((relative) => path.resolve(appRoot, relative));
|
|
245
|
+
const deadline = now() + timeoutMs;
|
|
246
|
+
let previous = files.map(signature);
|
|
247
|
+
let stable = 0;
|
|
248
|
+
while (stable < checks) {
|
|
249
|
+
if (now() >= deadline) fail("RECIPE_BUILD_ARTIFACT_UNSTABLE", `artifact missing or still changing after ${timeoutMs} ms: ${files.join(", ")}`, { files });
|
|
250
|
+
sleep(intervalMs);
|
|
251
|
+
const current = files.map(signature);
|
|
252
|
+
stable = current.every((value, index) => value !== null && value === previous[index]) ? stable + 1 : 0;
|
|
253
|
+
previous = current;
|
|
254
|
+
}
|
|
255
|
+
log(`recipe-build: artifacts stable (${files.length} file(s), ${checks} check(s) ${intervalMs} ms apart)`);
|
|
256
|
+
return files;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* 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
|
|
262
|
+
* nothing after it runs.
|
|
263
|
+
* Options: appRoot (absolute), env, platform, log, spawnSync, fs, sleep, now, statSync (injected in tests).
|
|
264
|
+
*/
|
|
265
|
+
export function runRecipeBuild(rawPlan, { appRoot, env = process.env, platform = process.platform, log = () => {}, spawnSync = nodeSpawnSync, fs, sleep, now, statSync } = {}) {
|
|
266
|
+
if (typeof appRoot !== "string" || !path.isAbsolute(appRoot)) fail("RECIPE_BUILD_PLAN_INVALID", "appRoot must be an absolute path");
|
|
267
|
+
const plan = validateRecipePlan(rawPlan);
|
|
268
|
+
const done = [];
|
|
269
|
+
const cleaned = runRecipeCleanup(plan.cleanup, { appRoot, platform, log, spawnSync, ...(fs ? { fs } : {}) });
|
|
270
|
+
let stable = [];
|
|
271
|
+
for (const [index, step] of plan.steps.entries()) {
|
|
272
|
+
const launch = resolveLaunch({ cmd: step.cmd, args: step.args, platform, env });
|
|
273
|
+
const cwd = path.resolve(appRoot, step.cwd ?? ".");
|
|
274
|
+
log(`recipe-build: [${step.phase}] ${step.cmd} ${step.args.join(" ")}`.trimEnd());
|
|
275
|
+
const result = spawnSync(launch.command, launch.args, { cwd, env: { ...env, ...(step.env ?? {}) }, stdio: "inherit", shell: launch.shell, windowsHide: true });
|
|
276
|
+
if (result?.error || result?.status !== 0) {
|
|
277
|
+
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
|
+
}
|
|
279
|
+
done.push({ phase: step.phase, cmd: step.cmd, status: 0 });
|
|
280
|
+
if (step.phase === "build") stable = waitForStableArtifacts(plan.stability, { appRoot, log, ...(sleep ? { sleep } : {}), ...(now ? { now } : {}), ...(statSync ? { statSync } : {}) });
|
|
281
|
+
}
|
|
282
|
+
return { steps: done, cleaned, stable };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function parseRecipeBuildArgs(argv) {
|
|
286
|
+
const opts = { plan: undefined, planFile: undefined, appRoot: undefined };
|
|
287
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
288
|
+
const arg = argv[i];
|
|
289
|
+
if (arg === "--plan" || arg === "--plan-file" || arg === "--app-root") {
|
|
290
|
+
const value = argv[i + 1];
|
|
291
|
+
if (value === undefined || (arg !== "--plan" && value.startsWith("--"))) throw new Error(`${arg} needs a value`);
|
|
292
|
+
opts[arg === "--plan" ? "plan" : arg === "--plan-file" ? "planFile" : "appRoot"] = value;
|
|
293
|
+
i += 1;
|
|
294
|
+
} else throw new Error(`unknown recipe-build argument: ${arg}`);
|
|
295
|
+
}
|
|
296
|
+
if ((opts.plan === undefined) === (opts.planFile === undefined)) throw new Error("recipe-build needs exactly one of --plan <json> or --plan-file <path>");
|
|
297
|
+
return opts;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* The commands a build command really runs. A declarative winPackage / macPackage wraps the Tauri build as
|
|
302
|
+
* `node recipe-build.mjs --plan <JSON>` (or `--plan-file <path>`), which hides each step's `--features` inside one JSON
|
|
303
|
+
* argument. The forbidden-feature scan and the cargo-tree closure must see every step, so this returns
|
|
304
|
+
* `[command, ...every prepare / build / postBuild step as { cmd, args, env? }]` (nested wrappers are expanded too), and
|
|
305
|
+
* `[command]` for any other command. A wrapper whose plan cannot be read or validated throws (fail closed).
|
|
306
|
+
* Options: root (the app root, for --plan-file and --app-root), readFile (injected in tests).
|
|
307
|
+
*/
|
|
308
|
+
export function expandRecipeBuildCommands(command, { root = process.cwd(), readFile = readFileSync, depth = 0 } = {}) {
|
|
309
|
+
if (command === undefined || command === null) return [];
|
|
310
|
+
const parts = typeof command === "string"
|
|
311
|
+
? (command.match(/"[^"]*"|'[^']*'|[^\s"']+/g) ?? []).map((token) => token.replace(/^["']|["']$/g, ""))
|
|
312
|
+
: [command?.cmd, ...(command?.args ?? [])].filter((value) => value !== undefined).map(String);
|
|
313
|
+
const at = parts.findIndex((part) => path.basename(part.replaceAll("\\", "/")) === "recipe-build.mjs" || part === "recipe-build");
|
|
314
|
+
if (at < 0) return [command];
|
|
315
|
+
const rest = parts.slice(at + 1);
|
|
316
|
+
if (!rest.some((arg) => arg === "--plan" || arg === "--plan-file")) return [command];
|
|
317
|
+
if (depth >= 3) fail("RECIPE_BUILD_PLAN_INVALID", "recipe-build plans are nested too deeply to scan");
|
|
318
|
+
let plan;
|
|
319
|
+
try {
|
|
320
|
+
const opts = parseRecipeBuildArgs(rest);
|
|
321
|
+
const text = opts.plan ?? readFile(path.resolve(path.resolve(root, opts.appRoot ?? "."), opts.planFile), "utf8");
|
|
322
|
+
plan = validateRecipePlan(JSON.parse(text));
|
|
323
|
+
} catch (error) {
|
|
324
|
+
if (error instanceof RecipeBuildError) throw error;
|
|
325
|
+
fail("RECIPE_BUILD_PLAN_INVALID", `cannot scan the recipe-build command (${error.message}); every step must be visible to the forbidden-feature scan`);
|
|
326
|
+
}
|
|
327
|
+
const steps = plan.steps.map(({ cmd, args, env }) => ({ cmd, args, ...(env ? { env } : {}) }));
|
|
328
|
+
return [command, ...steps.flatMap((step) => expandRecipeBuildCommands(step, { root, readFile, depth: depth + 1 }))];
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async function main() {
|
|
332
|
+
const opts = parseRecipeBuildArgs(process.argv.slice(2));
|
|
333
|
+
const appRoot = path.resolve(opts.appRoot ?? process.cwd());
|
|
334
|
+
let text = opts.plan;
|
|
335
|
+
if (text === undefined) {
|
|
336
|
+
const { readFileSync } = await import("node:fs");
|
|
337
|
+
text = readFileSync(path.resolve(appRoot, opts.planFile), "utf8");
|
|
338
|
+
}
|
|
339
|
+
let plan;
|
|
340
|
+
try { plan = JSON.parse(text); } catch (error) { throw new RecipeBuildError("RECIPE_BUILD_PLAN_INVALID", `the plan is not JSON: ${error.message}`); }
|
|
341
|
+
runRecipeBuild(plan, { appRoot, log: (line) => console.log(line) });
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
|
345
|
+
main().catch((error) => { console.error(`right-release recipe-build: ${error.code ? `[${error.code}] ` : ""}${error.message}`); process.exitCode = 1; });
|
|
346
|
+
}
|
package/release.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { expandPackageRecipeTarget } from "./package-recipe-targets.mjs";
|
|
2
3
|
import { access, readFile, readdir } from "node:fs/promises";
|
|
3
4
|
import { createHash } from "node:crypto";
|
|
4
5
|
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
@@ -45,6 +46,7 @@ import { DEFAULT_STORAGE_THRESHOLD_BYTES, findInstallerCopyScripts, findLargeRep
|
|
|
45
46
|
import { macosCodesignEnvironment } from "./codesign-environment.mjs";
|
|
46
47
|
import { assertNoQaControl, assertNoWebBundleMocks } from "./qa-control-scan.mjs";
|
|
47
48
|
import { assertNoForbiddenBuildFeatures } from "./qa-build-features.mjs";
|
|
49
|
+
import { expandRecipeBuildCommands } from "./recipe-build.mjs";
|
|
48
50
|
import { assertNoReleaseSwitches, commandTokens, resolveFeatureClosure } from "./release-switch-scan.mjs";
|
|
49
51
|
import { devtoolsEvidence, resolveDevtoolsAllowance } from "./devtools-policy.mjs";
|
|
50
52
|
import { runAndroidRelease, validateAndroidTarget } from "./android-release.mjs";
|
|
@@ -189,7 +191,9 @@ validateRightKitCargoContract(root, RIGHTKIT_CARGO_ALLOWED, config.app ?? path.b
|
|
|
189
191
|
assertCargoProfileContract(root, config.app ?? path.basename(root));
|
|
190
192
|
// RightKit QA is mandatory for every app (also SwiftPM apps without a package.json): no QA lane refuses the release.
|
|
191
193
|
assertQaAdoption({ appRoot: root, app: config.app });
|
|
192
|
-
|
|
194
|
+
let target = config.targets?.[opts.platform];
|
|
195
|
+
// Declarative winPackage / macPackage (#345) expand to the same hook shape every gate below already checks.
|
|
196
|
+
try { target = expandPackageRecipeTarget(target, { platform: opts.platform, appRoot: root, config }); } catch (error) { fail(`${config.app ?? "app"} ${opts.platform} ${error.message}`); }
|
|
193
197
|
if (!target) fail(`${config.app ?? "app"} has no ${opts.platform} release target`);
|
|
194
198
|
const legalContract = config.legal
|
|
195
199
|
? assertLegalReleaseContract(root, config.legal, config.app ?? path.basename(root), opts.platform)
|
|
@@ -353,7 +357,7 @@ try {
|
|
|
353
357
|
{ label: "target.package", command: target.package },
|
|
354
358
|
{ label: "nativeAssembly.packageHook", command: nativeAssembly?.packageHook },
|
|
355
359
|
{ label: "nativeAssembly.finalizer", command: nativeFinalizer },
|
|
356
|
-
{ label:
|
|
360
|
+
...commandList(target.postSign).map((command, index) => ({ label: Array.isArray(target.postSign) ? `target.postSign[${index}]` : "target.postSign", command })),
|
|
357
361
|
{ label: "target.gradle", command: target.gradle },
|
|
358
362
|
] });
|
|
359
363
|
} catch (error) { fail(error.message); }
|
|
@@ -367,6 +371,9 @@ try {
|
|
|
367
371
|
const releaseSwitchInventoryPath = path.resolve(root, config.releaseSwitches?.inventory ?? "switches.json");
|
|
368
372
|
const releaseSwitchInventoryText = existsSync(releaseSwitchInventoryPath) ? readFileSync(releaseSwitchInventoryPath, "utf8") : null;
|
|
369
373
|
try {
|
|
374
|
+
// A `node recipe-build.mjs --plan <JSON>` prePackage hides each step's `--features` in one argument: the closure and the
|
|
375
|
+
// command token scan see every prepare / build / postBuild step (a plan that cannot be read or validated fails closed).
|
|
376
|
+
const closureCommands = [target.prePackage, target.package, nativeAssembly?.packageHook, nativeFinalizer, ...commandList(target.postSign), target.gradle].filter(Boolean).flatMap((c) => expandRecipeBuildCommands(c, { root }));
|
|
370
377
|
let treeText;
|
|
371
378
|
try {
|
|
372
379
|
const closure = resolveFeatureClosure({
|
|
@@ -376,7 +383,7 @@ try {
|
|
|
376
383
|
rust: config.releaseSwitches?.rust,
|
|
377
384
|
features: config.releaseSwitches?.features ?? [],
|
|
378
385
|
triple: target.cargoTarget ?? target.targetTriple ?? target.rustTarget,
|
|
379
|
-
commandTexts:
|
|
386
|
+
commandTexts: closureCommands,
|
|
380
387
|
});
|
|
381
388
|
if (closure.skipped) console.log(`right-release: feature closure skipped (${closure.skipped})`);
|
|
382
389
|
else treeText = closure.treeText;
|
|
@@ -385,7 +392,7 @@ try {
|
|
|
385
392
|
console.log(`right-release: dry run: ${error.message}`);
|
|
386
393
|
}
|
|
387
394
|
// remoteHost: rightkit-daemon `remote` is refused in the closure unless the app declares releaseSwitches.remoteHost: true.
|
|
388
|
-
const closureInput = { treeText, remoteHost: config.releaseSwitches?.remoteHost, commands:
|
|
395
|
+
const closureInput = { treeText, remoteHost: config.releaseSwitches?.remoteHost, commands: closureCommands.map((c) => commandTokens(c)) };
|
|
389
396
|
if (releaseSwitchInventoryText != null) {
|
|
390
397
|
assertNoReleaseSwitches({ ...closureInput, sourceRoot: root, inventoryText: releaseSwitchInventoryText, appForbid: config.releaseSwitches?.forbid ?? [] }, "source switch inventory");
|
|
391
398
|
} else if (treeText != null || closureInput.commands.length) {
|
|
@@ -491,7 +498,7 @@ if (opts.platform === "win" && target.signingContract === MSIX_SIGNING_CONTRACT)
|
|
|
491
498
|
assertNoQaControlBinaries("macOS app staged for the DMG", [stagedApp], { requireBinary: true, prePack: true });
|
|
492
499
|
}
|
|
493
500
|
if (runMacPhase("sign")) {
|
|
494
|
-
await signMacos(signingEntries, root, "signing", macSigning.nestedCode);
|
|
501
|
+
await signMacos(signingEntries, root, "signing", macSigning.nestedCode, macSigning.nestedSigning);
|
|
495
502
|
if (sidecarPlan && !opts.dryRun) {
|
|
496
503
|
const app = signingEntries.find((entry) => /\.app\/?$/.test(entry.file));
|
|
497
504
|
if (!app) fail("mediaSidecar on macOS needs an .app entry in sign.prePackageFiles");
|
|
@@ -560,14 +567,14 @@ if (opts.platform === "win" && target.signingContract === WINDOWS_NSIS_SIGNING_C
|
|
|
560
567
|
// and still installs a binary Windows rejects.
|
|
561
568
|
await verifyWindowsEmbedding({ root, target, files, markers: bundleMarkerReceipts });
|
|
562
569
|
await signWindows(files, "installer", root);
|
|
563
|
-
if (target.postSign) await
|
|
570
|
+
if (target.postSign) await runCommands(target.postSign, root);
|
|
564
571
|
const updaterFiles = [...new Set((target.updater?.artifacts ?? []).map((artifact) => resolveReleaseArtifactPath(root, artifact.file)))];
|
|
565
572
|
if (!updaterFiles.length) fail(`${config.app} win must declare updater.artifacts for post-code-signing minisign`);
|
|
566
573
|
await run("node", [SIGN_UPDATER, ...(opts.dryRun ? ["--dry-run"] : []), ...(target.updater?.keyFile ? ["--key-file", String(target.updater.keyFile)] : []), ...updaterFiles.flatMap((file) => ["--file", file])], root);
|
|
567
574
|
}
|
|
568
575
|
|
|
569
576
|
if (target.postSign && opts.platform !== "win" && (opts.platform !== "mac" || runMacPhase("post-sign"))) {
|
|
570
|
-
await
|
|
577
|
+
await runCommands(target.postSign, root);
|
|
571
578
|
}
|
|
572
579
|
|
|
573
580
|
if (opts.platform === "mac") {
|
|
@@ -930,6 +937,17 @@ async function runPackageScript(pm, script, cwd) {
|
|
|
930
937
|
await run(pm, ["run", script], cwd);
|
|
931
938
|
}
|
|
932
939
|
|
|
940
|
+
// A hook may be one command object or an ordered array of them (e.g. create-mac-updater then lsclean).
|
|
941
|
+
function commandList(value) {
|
|
942
|
+
if (value == null) return [];
|
|
943
|
+
return Array.isArray(value) ? value : [value];
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// Runs in order; run() exits the process on the first failure, so the chain fails closed.
|
|
947
|
+
async function runCommands(value, root, options) {
|
|
948
|
+
for (const command of commandList(value)) await runCommand(command, root, options);
|
|
949
|
+
}
|
|
950
|
+
|
|
933
951
|
async function runCommand(command, root, { env = {}, captureStdout = false } = {}) {
|
|
934
952
|
const cwd = path.resolve(root, command.cwd ?? ".");
|
|
935
953
|
return run(command.cmd, command.args ?? [], cwd, { ...(await commandEnv(command, root)), ...env }, {
|
|
@@ -1459,12 +1477,12 @@ function macSignInvocation(items, receiptPath, extra = []) {
|
|
|
1459
1477
|
return [SIGN_MACOS, ...extra, "--receipt", receiptPath, "--manifest", manifest];
|
|
1460
1478
|
}
|
|
1461
1479
|
|
|
1462
|
-
async function signMacos(entries, root, phase = "signing", nestedCode = []) {
|
|
1480
|
+
async function signMacos(entries, root, phase = "signing", nestedCode = [], nestedSigning) {
|
|
1463
1481
|
const files = entries.map((entry) => entry.file);
|
|
1464
1482
|
const receipt = phase === "signing" ? macosReceiptPath(root, "signing") : path.resolve(process.env.RIGHT_RELEASE_STATE_ROOT ?? path.join(root, ".right-release", "receipts"), `macos-${phase}.json`);
|
|
1465
1483
|
// Directory entries (.app bundles): nested code first (default settings unless sign.nestedCode names it), then the bundle itself.
|
|
1466
1484
|
let nested = [];
|
|
1467
|
-
try { ({ nested } = opts.dryRun ? { nested: [] } : planMacSigning({ entries, nestedCode })); } catch (error) { fail(error.message); }
|
|
1485
|
+
try { ({ nested } = opts.dryRun ? { nested: [] } : planMacSigning({ entries, nestedCode, nestedSigning, root })); } catch (error) { fail(error.message); }
|
|
1468
1486
|
if (opts.dryRun && (nestedCode.length || entries.some(hasNonDefaultSettings))) {
|
|
1469
1487
|
console.log(`right-release: dry-run: per-file signing matrix (${nestedCode.length} nestedCode rule(s), ${entries.filter(hasNonDefaultSettings).length} entry setting(s)); nested code is resolved against the built bundle at sign time`);
|
|
1470
1488
|
}
|
|
@@ -11,6 +11,11 @@ import { parseCodesignArgv } from "./codesign-argv.mjs";
|
|
|
11
11
|
|
|
12
12
|
export const AGENT_LABEL = "dev.rightkit.signing-agent";
|
|
13
13
|
export const MAX_FRAME = 1024 * 1024;
|
|
14
|
+
// A typed codesign `--identifier` (reverse-DNS characters only; never starts with "-", so it cannot read as an option).
|
|
15
|
+
export const AGENT_IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9.-]{0,254}$/;
|
|
16
|
+
// Request schema 2 is codesign-only and is the sole schema that may carry per-item `identifier`. Schema 1 stays valid and
|
|
17
|
+
// byte-compatible for everything else; an agent that predates schema 2 refuses it (and any unknown item field) outright.
|
|
18
|
+
export const CODESIGN_IDENTIFIER_SCHEMA = 2;
|
|
14
19
|
export const NOTARIZATION_TIMEOUT_MS = 1800000;
|
|
15
20
|
export const MAX_NOTARIZATION_TIMEOUT_MS = 7200000;
|
|
16
21
|
export class SigningAgentError extends Error {
|
|
@@ -72,7 +77,7 @@ export function validateSigningRequest(request, { roots, stagingRoot, io = fs }
|
|
|
72
77
|
const notary = ["notarize", "staple"].includes(request?.type);
|
|
73
78
|
if (!["codesign", "codesign-argv", "updater", "notarize", "staple"].includes(request?.type)) fail("MAC_SIGNING_AGENT_PROTOCOL", "Unsupported signing request type");
|
|
74
79
|
shape(request, [...common, ...(notary ? ["path", "profile", "staple", "app", "validateOnly"] : request.type === "codesign-argv" ? ["argv", "targets"] : [request.type === "codesign" ? "items" : "files"])]);
|
|
75
|
-
if (request.schema
|
|
80
|
+
if (!(request.schema === 1 || (request.type === "codesign" && request.schema === CODESIGN_IDENTIFIER_SCHEMA)) || !Number.isInteger(request.timeoutMs) || request.timeoutMs < 1 || request.timeoutMs > (notary ? MAX_NOTARIZATION_TIMEOUT_MS : 180000)) {
|
|
76
81
|
fail("MAC_SIGNING_AGENT_PROTOCOL", "Invalid schema or signing deadline");
|
|
77
82
|
}
|
|
78
83
|
if (!Array.isArray(request.allowedRoots) || !request.allowedRoots.length || request.allowedRoots.length > 32) fail("MAC_SIGNING_AGENT_PATH_DENIED", "Allowed roots required");
|
|
@@ -142,7 +147,8 @@ export function validateSigningRequest(request, { roots, stagingRoot, io = fs }
|
|
|
142
147
|
}
|
|
143
148
|
if (!Array.isArray(request.items) || !request.items.length || request.items.length > 4096) fail("MAC_SIGNING_AGENT_PROTOCOL", "Codesign items required");
|
|
144
149
|
const items = request.items.map((item) => {
|
|
145
|
-
shape(item, ["path", "identity", "entitlements", "runtime", "timestamp", "force", "preserveEntitlements"]);
|
|
150
|
+
shape(item, ["path", "identity", "entitlements", "runtime", "timestamp", "force", "preserveEntitlements", ...(request.schema === CODESIGN_IDENTIFIER_SCHEMA ? ["identifier"] : [])]);
|
|
151
|
+
if (item.identifier !== undefined && (typeof item.identifier !== "string" || !AGENT_IDENTIFIER_PATTERN.test(item.identifier))) fail("MAC_SIGNING_AGENT_PROTOCOL", "Invalid codesign identifier");
|
|
146
152
|
if (typeof item.identity !== "string" || !item.identity.trim() || item.identity === "-" || item.identity.includes("\0") || item.identity.length > 512) fail("MAC_SIGNING_AGENT_PROTOCOL", "Real signing identity required");
|
|
147
153
|
for (const field of ["runtime", "timestamp", "force", "preserveEntitlements"]) if (item[field] !== undefined && typeof item[field] !== "boolean") fail("MAC_SIGNING_AGENT_PROTOCOL", "Signing options must be boolean");
|
|
148
154
|
return { ...item, path: checked(item.path), ...(item.entitlements !== undefined ? { entitlements: checked(item.entitlements, { entitlements: true }) } : {}) };
|
|
@@ -156,7 +162,7 @@ export function authenticateSigningToken(candidate, token) {
|
|
|
156
162
|
}
|
|
157
163
|
export function executeSigningRequest(request, { run = spawnSync, home = os.userInfo().homedir,
|
|
158
164
|
username = os.userInfo().username, pnpm, environmentPath, now = Date.now } = {}) {
|
|
159
|
-
if (request.type === "health") return { schema: 1, ok: true, service: AGENT_LABEL, pid: process.pid, capabilities: ["codesign", "codesign-argv", "updater", "notarize", "staple"] };
|
|
165
|
+
if (request.type === "health") return { schema: 1, ok: true, service: AGENT_LABEL, pid: process.pid, capabilities: ["codesign", "codesign-argv", "updater", "notarize", "staple", "codesign-identifier"] };
|
|
160
166
|
const deadline = now() + request.timeoutMs;
|
|
161
167
|
const env = { HOME: home, PATH: environmentPath, TMPDIR: path.join(signingAgentPaths(home).directory, "tmp") };
|
|
162
168
|
if (request.type === "codesign-argv") {
|
|
@@ -191,7 +197,7 @@ export function executeSigningRequest(request, { run = spawnSync, home = os.user
|
|
|
191
197
|
// Preserve caller's inside-out ordering. No arbitrary argv reaches spawn.
|
|
192
198
|
for (const item of request.items) {
|
|
193
199
|
const args = [...(item.force ? ["--force"] : []), ...(item.runtime ? ["--options", "runtime"] : []),
|
|
194
|
-
...(item.timestamp ? ["--timestamp"] : []), "--sign", item.identity,
|
|
200
|
+
...(item.timestamp ? ["--timestamp"] : []), ...(item.identifier !== undefined ? ["--identifier", item.identifier] : []), "--sign", item.identity,
|
|
195
201
|
...(item.entitlements ? ["--entitlements", item.entitlements] : []),
|
|
196
202
|
...(item.preserveEntitlements ? ["--preserve-metadata=entitlements"] : []), item.path];
|
|
197
203
|
results.push(invoke("/usr/bin/codesign", args).result);
|
package/rightkit-sync.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { fileURLToPath } from "node:url";
|
|
|
10
10
|
const packageRoot = path.dirname(fileURLToPath(import.meta.url));
|
|
11
11
|
const rightkitRoot = path.resolve(packageRoot, "..", "..");
|
|
12
12
|
const VERSIONS_FILE = path.join(packageRoot, "rightkit-versions.json");
|
|
13
|
-
const USER_AGENT = "rightkit-sync/1 (https://github.com/
|
|
13
|
+
const USER_AGENT = "rightkit-sync/1 (https://github.com/bogusyogi/rightkit)";
|
|
14
14
|
|
|
15
15
|
const field = (toml, key) => toml.match(new RegExp(`^${key}\\s*=\\s*"([^"]*)"`, "m"))?.[1] ?? null;
|
|
16
16
|
|
package/rightkit-versions.json
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"@rightkit/qa": "0.4.9",
|
|
32
32
|
"@rightkit/qa-chrome": "0.1.8",
|
|
33
33
|
"@rightkit/react-shell": "0.6.8",
|
|
34
|
-
"@rightkit/release": "0.2.
|
|
34
|
+
"@rightkit/release": "0.2.174",
|
|
35
35
|
"@rightkit/settings-ui": "0.5.0",
|
|
36
36
|
"@rightkit/tauri": "0.2.1",
|
|
37
37
|
"@rightkit/tauri-shell": "0.1.1",
|
|
@@ -209,7 +209,9 @@
|
|
|
209
209
|
"0.2.162",
|
|
210
210
|
"0.2.163",
|
|
211
211
|
"0.2.164",
|
|
212
|
-
"0.2.165"
|
|
212
|
+
"0.2.165",
|
|
213
|
+
"0.2.172",
|
|
214
|
+
"0.2.173"
|
|
213
215
|
],
|
|
214
216
|
"@rightkit/qa": [
|
|
215
217
|
"0.1.0",
|
|
@@ -383,7 +385,7 @@
|
|
|
383
385
|
"rightkit-automation": "0.4.1",
|
|
384
386
|
"rightkit-browser": "0.3.1",
|
|
385
387
|
"rightkit-cache": "0.2.0",
|
|
386
|
-
"rightkit-channel-memory": "0.1
|
|
388
|
+
"rightkit-channel-memory": "0.2.1",
|
|
387
389
|
"rightkit-child-process": "0.4.3",
|
|
388
390
|
"rightkit-compose": "0.1.0",
|
|
389
391
|
"rightkit-contracts": "0.1.5",
|
|
@@ -455,7 +457,7 @@
|
|
|
455
457
|
"rightkit-automation": "0.4.1",
|
|
456
458
|
"rightkit-browser": "0.3.1",
|
|
457
459
|
"rightkit-cache": "0.2.0",
|
|
458
|
-
"rightkit-channel-memory": "0.1
|
|
460
|
+
"rightkit-channel-memory": "0.2.1",
|
|
459
461
|
"rightkit-child-process": "0.4.1",
|
|
460
462
|
"rightkit-compose": "0.1.0",
|
|
461
463
|
"rightkit-contracts": "0.1.5",
|
|
@@ -537,10 +539,10 @@
|
|
|
537
539
|
"@rightkit/outdated": "0.2.0",
|
|
538
540
|
"@rightkit/pdf-ui": "0.1.8",
|
|
539
541
|
"@rightkit/platform-ui": "0.2.8",
|
|
540
|
-
"@rightkit/qa": "0.4.
|
|
542
|
+
"@rightkit/qa": "0.4.9",
|
|
541
543
|
"@rightkit/qa-chrome": "0.1.8",
|
|
542
544
|
"@rightkit/react-shell": "0.6.8",
|
|
543
|
-
"@rightkit/release": "0.2.
|
|
545
|
+
"@rightkit/release": "0.2.173",
|
|
544
546
|
"@rightkit/settings-ui": "0.5.0",
|
|
545
547
|
"@rightkit/shell": "0.1.1",
|
|
546
548
|
"@rightkit/tauri": "0.2.1",
|
|
@@ -811,6 +813,9 @@
|
|
|
811
813
|
],
|
|
812
814
|
"rightkit-hotkeys": [
|
|
813
815
|
"0.1.0"
|
|
816
|
+
],
|
|
817
|
+
"rightkit-channel-memory": [
|
|
818
|
+
"0.1.0"
|
|
814
819
|
]
|
|
815
820
|
}
|
|
816
821
|
}
|