@rightkit/release 0.2.165 → 0.2.173
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 +121 -4
- package/addon-command.mjs +6 -13
- package/android-aab-scan.mjs +265 -0
- package/android-dev.mjs +111 -0
- package/android-ndk.mjs +77 -0
- package/android-play-upload.mjs +262 -0
- package/android-release.mjs +344 -0
- package/android-test-fixtures.mjs +79 -0
- package/apple-profile-create.mjs +363 -0
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/darwin/rightkit-swap.sign.json +6 -0
- package/build-release.mjs +73 -13
- package/cargo-guard.mjs +2 -0
- package/cargo-profile-contract.mjs +183 -0
- package/cargo-target.mjs +8 -0
- package/cli/right-release.mjs +16 -4
- package/committed-source.mjs +1 -1
- package/dev-artifact-refresh.mjs +176 -0
- package/dev-artifact-support.mjs +61 -0
- package/development-signing.mjs +71 -4
- package/development.mjs +93 -47
- package/direct-bootstrap.mjs +26 -41
- package/disk-command.mjs +298 -5
- package/disk-policy.json +7 -5
- package/disk-policy.mjs +105 -16
- package/disk-stable-root.mjs +19 -16
- package/engine-locator.mjs +116 -0
- package/external-bin.mjs +133 -0
- package/github-release.mjs +2 -2
- package/github-updater-release.mjs +154 -0
- package/hardeningscan.mjs +1 -1
- package/in-place-install.mjs +56 -0
- package/install-app-windows.mjs +63 -0
- package/install-app.mjs +53 -23
- package/install-tauri-devid.mjs +172 -0
- package/inventory-check.mjs +1 -1
- package/local-publish.mjs +11 -1
- package/mac-package-recipe.mjs +304 -0
- package/mac-signing-matrix.mjs +23 -11
- package/managed-cargo.mjs +6 -0
- package/media-sidecar-catalog.mjs +45 -0
- package/media-sidecar.mjs +306 -0
- package/ownership-approval-command.mjs +10 -44
- package/ownership-approval.mjs +3 -42
- package/ownership-catalog-config.json +2 -1
- package/ownership-catalog.mjs +4 -2
- package/ownership-command.mjs +7 -9
- package/ownership-dependency-allowlist.json +25 -1
- package/ownership-evidence.mjs +8 -5
- package/ownership-git-inventory.mjs +41 -7
- package/ownership-lockfiles.mjs +91 -8
- package/ownership-mandatory-set.json +36 -16
- package/ownership-report.mjs +5 -6
- package/ownership-scan-worker.mjs +2 -2
- package/ownership-verdict.mjs +10 -7
- package/ownership.mjs +47 -29
- package/package-recipe-targets.mjs +95 -0
- package/package.json +7 -4
- package/portable-development.mjs +32 -115
- package/preflight.mjs +4 -6
- package/project-type.mjs +1 -1
- package/publication-artifacts.mjs +34 -0
- package/publish-update.mjs +72 -11
- package/qa-adoption-exemptions.json +5 -0
- package/qa-build-features.mjs +7 -0
- package/qa-contract.mjs +182 -0
- package/qa-control-scan.mjs +3 -3
- package/release-qa-command.mjs +38 -12
- package/release-qa-installed.mjs +136 -0
- package/release-qa-receipt.mjs +23 -2
- package/release.mjs +285 -14
- package/rightkit-ownership.json +537 -5
- package/rightkit-sync.mjs +27 -1
- package/rightkit-versions.json +111 -63
- package/runtime-budget-classes.json +28 -0
- package/runtime-budgets.mjs +4 -1
- package/sign-candidate.mjs +161 -0
- package/sign-dev-artifact.mjs +198 -0
- package/stage-binary.mjs +60 -0
- package/stage-sidecar-command.mjs +41 -0
- package/stage-sidecar.mjs +424 -0
- package/swap-helper-bundle.mjs +105 -0
- package/swift-mac-dmg.mjs +2 -1
- package/templates/cargo-profiles.toml +28 -0
- package/upload-release.mjs +11 -1
- package/win-package-recipe.mjs +321 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// right-release sign-dev-artifact: Developer ID sign an EXISTING, already-qualified CI dev artifact in place (Orthic-Labs/rightkit#257).
|
|
3
|
+
// It never builds, notarizes, publishes or installs. Steps: verify the artifact's recorded hashes, sign each manifest file with its fixed
|
|
4
|
+
// codesign identifier (mac-sign-core signMacosEntries, the provisioned Developer ID only), verify strictly, read back Identifier and
|
|
5
|
+
// TeamIdentifier, refresh the payload metadata, and write evidence JSON. An optional declarative `refresh` in the sign manifest
|
|
6
|
+
// (dev-artifact-refresh.mjs) recomputes digests recorded in JSON files after the code they describe is signed and before the bundle
|
|
7
|
+
// that contains those files is signed, so that bundle seals their final bytes; the payload metadata refresh then records everything.
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { pathToFileURL } from "node:url";
|
|
11
|
+
import { spawnSync } from "./background-process.mjs";
|
|
12
|
+
import { MacosDevelopmentSigningError, resolveMacosDevelopmentSigning } from "./development-signing.mjs";
|
|
13
|
+
import { SIGN_MANIFEST_SCHEMA, parseSignManifest, signMacosEntries } from "./mac-sign-core.mjs";
|
|
14
|
+
import { fileEvidence } from "./macos-file-evidence.mjs";
|
|
15
|
+
import { MACOS_PORTABLE_SIGNING_CONTRACT } from "./macos-signing-contract.mjs";
|
|
16
|
+
import { runMacCodesignCommands } from "./macos-signing-session.mjs";
|
|
17
|
+
import { SignDevArtifactError, assertInsideOnDisk, contained, fail, toPosix } from "./dev-artifact-support.mjs";
|
|
18
|
+
import { applyRefresh, planRefresh, readRefreshPlan } from "./dev-artifact-refresh.mjs";
|
|
19
|
+
|
|
20
|
+
export const DEV_ARTIFACT_PAYLOAD_SCHEMA = 1;
|
|
21
|
+
export const DEV_ARTIFACT_EVIDENCE_SCHEMA = 1;
|
|
22
|
+
export const DEFAULT_PAYLOAD_METADATA = "payload.json";
|
|
23
|
+
|
|
24
|
+
export { SignDevArtifactError };
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Sign manifest for this command: `{ schema: 1, entries: [{ file, identifier, entitlements?, options?, hardenedRuntime? }] }`.
|
|
28
|
+
* `file` is relative to the artifact directory (a loose executable or the installer .app); `identifier` is required on every entry.
|
|
29
|
+
* Entries sign in the given order, so list nested code before the bundle that contains it. Returns absolute-path entries.
|
|
30
|
+
*/
|
|
31
|
+
export function readDevArtifactSignManifest(file, artifact) {
|
|
32
|
+
let raw;
|
|
33
|
+
try { raw = JSON.parse(readFileSync(file, "utf8")); } catch { fail("SIGN_DEV_ARTIFACT_MANIFEST_INVALID", `sign manifest is not readable JSON: ${file}`); }
|
|
34
|
+
if (raw?.schema !== SIGN_MANIFEST_SCHEMA || !Array.isArray(raw.entries) || !raw.entries.length) {
|
|
35
|
+
fail("SIGN_DEV_ARTIFACT_MANIFEST_INVALID", `sign manifest must be { schema: ${SIGN_MANIFEST_SCHEMA}, entries: [...] } with at least one entry`);
|
|
36
|
+
}
|
|
37
|
+
const absolute = raw.entries.map((entry, index) => {
|
|
38
|
+
if (!entry || typeof entry !== "object") fail("SIGN_DEV_ARTIFACT_MANIFEST_INVALID", `sign manifest entry ${index} must be an object`);
|
|
39
|
+
if (entry.identifier === undefined) fail("SIGN_DEV_ARTIFACT_IDENTIFIER_REQUIRED", `sign manifest entry ${index} (${entry.file}) needs a fixed identifier`);
|
|
40
|
+
return {
|
|
41
|
+
...entry,
|
|
42
|
+
file: contained(artifact, entry.file, `sign manifest entry ${index} file`),
|
|
43
|
+
...(entry.entitlements !== undefined ? { entitlements: contained(artifact, entry.entitlements, `sign manifest entry ${index} entitlements`) } : {}),
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
const entries = parseSignManifest(JSON.stringify({ schema: SIGN_MANIFEST_SCHEMA, entries: absolute }));
|
|
47
|
+
const seen = new Set();
|
|
48
|
+
entries.forEach((entry, index) => {
|
|
49
|
+
if (seen.has(entry.file)) fail("SIGN_DEV_ARTIFACT_MANIFEST_INVALID", `sign manifest lists ${entry.file} twice`);
|
|
50
|
+
seen.add(entry.file);
|
|
51
|
+
for (const earlier of entries.slice(0, index)) {
|
|
52
|
+
if (entry.file.startsWith(earlier.file + path.sep)) fail("SIGN_DEV_ARTIFACT_ORDER_INVALID", `${path.relative(artifact, earlier.file)} contains ${path.relative(artifact, entry.file)} but is listed first; sign nested code before its bundle`);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
return entries;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The optional declarative `refresh` of the sign manifest (see dev-artifact-refresh.mjs): `{ root, operations }`, absolute paths, validated. */
|
|
59
|
+
export function readDevArtifactRefreshPlan(file, artifact) {
|
|
60
|
+
let raw;
|
|
61
|
+
try { raw = JSON.parse(readFileSync(file, "utf8")); } catch { fail("SIGN_DEV_ARTIFACT_MANIFEST_INVALID", `sign manifest is not readable JSON: ${file}`); }
|
|
62
|
+
return readRefreshPlan(raw, artifact);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Read the artifact's payload metadata `{ schema: 1, files: [{ path, sha256, ... }] }` and verify every recorded hash against disk. */
|
|
66
|
+
export function verifyDevArtifactPayload(artifact, metadataFile) {
|
|
67
|
+
let metadata;
|
|
68
|
+
try { metadata = JSON.parse(readFileSync(metadataFile, "utf8")); } catch { fail("SIGN_DEV_ARTIFACT_METADATA_INVALID", `payload metadata is not readable JSON: ${metadataFile}`); }
|
|
69
|
+
if (metadata?.schema !== DEV_ARTIFACT_PAYLOAD_SCHEMA || !Array.isArray(metadata.files) || !metadata.files.length) {
|
|
70
|
+
fail("SIGN_DEV_ARTIFACT_METADATA_INVALID", `payload metadata must be { schema: ${DEV_ARTIFACT_PAYLOAD_SCHEMA}, files: [...] } with at least one file: ${metadataFile}`);
|
|
71
|
+
}
|
|
72
|
+
const recorded = new Map();
|
|
73
|
+
for (const item of metadata.files) {
|
|
74
|
+
if (!/^[0-9a-f]{64}$/.test(item?.sha256 ?? "")) fail("SIGN_DEV_ARTIFACT_METADATA_INVALID", `payload metadata entry ${JSON.stringify(item?.path)} has no sha256`);
|
|
75
|
+
const absolute = contained(artifact, item.path, "payload metadata path");
|
|
76
|
+
if (!existsSync(absolute)) fail("SIGN_DEV_ARTIFACT_MISSING", `payload file is missing: ${item.path}`);
|
|
77
|
+
const actual = fileEvidence(absolute).sha256;
|
|
78
|
+
if (actual !== item.sha256) fail("SIGN_DEV_ARTIFACT_HASH_MISMATCH", `recorded hash does not match ${item.path}: recorded ${item.sha256}, found ${actual}. Refusing to sign bytes that were not qualified.`);
|
|
79
|
+
recorded.set(absolute, item);
|
|
80
|
+
}
|
|
81
|
+
return { metadata, recorded };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const defaultInspect = (run, env) => (command, args) => {
|
|
85
|
+
const result = run(command, args, { env, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], windowsHide: true, shell: false });
|
|
86
|
+
const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`;
|
|
87
|
+
if (result.error || result.status !== 0) fail("SIGN_DEV_ARTIFACT_INSPECT_FAILED", `${command} ${args.join(" ")} failed: ${output.trim() || result.error?.message}`);
|
|
88
|
+
return output;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Sign an existing dev artifact. Injectable for tests: `resolveSigning`, `signOne`, `inspect`, `evidenceOf`.
|
|
93
|
+
* Returns the evidence object (also written to `evidencePath`). With `dryRun` only the hash verification and manifest parsing run.
|
|
94
|
+
*/
|
|
95
|
+
export function signDevArtifact({
|
|
96
|
+
artifact, manifestPath, metadataPath, evidencePath, dryRun = false,
|
|
97
|
+
env = process.env, run = spawnSync, platform = process.platform,
|
|
98
|
+
resolveSigning = (options) => resolveMacosDevelopmentSigning(options),
|
|
99
|
+
signOne = (codesignArgs) => runMacCodesignCommands([codesignArgs], { env, run })[0],
|
|
100
|
+
inspect = defaultInspect(run, env), evidenceOf = fileEvidence,
|
|
101
|
+
} = {}) {
|
|
102
|
+
if (!artifact || !manifestPath) fail("SIGN_DEV_ARTIFACT_USAGE", "usage: right-release sign-dev-artifact --artifact <dir> --manifest <sign-manifest.json> [--metadata <payload.json>] [--evidence <path>] [--dry-run]");
|
|
103
|
+
const root = path.resolve(artifact);
|
|
104
|
+
if (!existsSync(root)) fail("SIGN_DEV_ARTIFACT_MISSING", `artifact directory does not exist: ${root}`);
|
|
105
|
+
const metadataFile = path.resolve(metadataPath ?? path.join(root, DEFAULT_PAYLOAD_METADATA));
|
|
106
|
+
const entries = readDevArtifactSignManifest(path.resolve(manifestPath), root);
|
|
107
|
+
const refreshPlan = readDevArtifactRefreshPlan(path.resolve(manifestPath), root);
|
|
108
|
+
const plan = planRefresh(entries, refreshPlan.operations, metadataFile);
|
|
109
|
+
const before = verifyDevArtifactPayload(root, metadataFile);
|
|
110
|
+
for (const entry of entries) {
|
|
111
|
+
if (!existsSync(entry.file)) fail("SIGN_DEV_ARTIFACT_MISSING", `sign manifest file is missing: ${path.relative(root, entry.file)}`);
|
|
112
|
+
if (!before.recorded.has(entry.file)) fail("SIGN_DEV_ARTIFACT_NOT_RECORDED", `${path.relative(root, entry.file)} is not in the payload metadata, so its hash was never recorded`);
|
|
113
|
+
if (entry.entitlements && !existsSync(entry.entitlements)) fail("SIGN_DEV_ARTIFACT_MISSING", `entitlements file is missing: ${path.relative(root, entry.entitlements)}`);
|
|
114
|
+
}
|
|
115
|
+
if (dryRun) return { schema: DEV_ARTIFACT_EVIDENCE_SCHEMA, dryRun: true, files: entries.map((entry) => ({ path: toPosix(path.relative(root, entry.file)), identifier: entry.identifier })) };
|
|
116
|
+
if (platform !== "darwin") fail("SIGN_DEV_ARTIFACT_PLATFORM", "sign-dev-artifact must run on macOS");
|
|
117
|
+
if (!evidencePath) fail("SIGN_DEV_ARTIFACT_USAGE", "--evidence requires a path");
|
|
118
|
+
|
|
119
|
+
// Same Developer ID resolution as dev signing: no ad hoc and no Apple Development fallback.
|
|
120
|
+
const signing = resolveSigning({ root, bundle: root, descriptor: {}, config: {}, env, run });
|
|
121
|
+
if (signing.adhoc || !/^Developer ID Application:/.test(signing.identity ?? "")) {
|
|
122
|
+
throw new MacosDevelopmentSigningError("DEV_MAC_SIGNING_DEVELOPER_ID_REQUIRED", `sign-dev-artifact needs the provisioned Developer ID Application identity; got ${JSON.stringify(signing.identity)}. There is no ad hoc or Apple Development fallback.`, { identity: signing.identity });
|
|
123
|
+
}
|
|
124
|
+
const { identity, teamIdentifier } = signing;
|
|
125
|
+
|
|
126
|
+
const sha256Before = new Map(entries.map((entry) => [entry.file, evidenceOf(entry.file).sha256]));
|
|
127
|
+
// Entries before the refresh sign first, then the declared metadata is recomputed from their final bytes, then the entries that
|
|
128
|
+
// contain the refreshed files (the wrapper bundle) sign over them. Without a refresh this is one signing pass, as before.
|
|
129
|
+
const signingOptions = { identity, signOne, inspect, evidence: evidenceOf, bundle: root };
|
|
130
|
+
// The manifest paths were checked when it was read; check them on disk again right before codesign touches them.
|
|
131
|
+
const recheck = (list) => {
|
|
132
|
+
for (const entry of list) {
|
|
133
|
+
assertInsideOnDisk(root, entry.file, `sign manifest file ${path.relative(root, entry.file)}`);
|
|
134
|
+
if (entry.entitlements) assertInsideOnDisk(root, entry.entitlements, `entitlements ${path.relative(root, entry.entitlements)}`);
|
|
135
|
+
}
|
|
136
|
+
return list;
|
|
137
|
+
};
|
|
138
|
+
const signed = plan.before.length ? signMacosEntries(recheck(plan.before), signingOptions) : [];
|
|
139
|
+
const refreshOps = refreshPlan.operations.length ? applyRefresh(refreshPlan.operations, refreshPlan.root) : [];
|
|
140
|
+
if (plan.after.length) signed.push(...signMacosEntries(recheck(plan.after), signingOptions));
|
|
141
|
+
const files = signed.map((record) => {
|
|
142
|
+
const rel = toPosix(path.relative(root, record.file));
|
|
143
|
+
const details = inspect("codesign", ["-dvv", record.file]);
|
|
144
|
+
const team = /^TeamIdentifier=(.+)$/m.exec(details)?.[1]?.trim();
|
|
145
|
+
const identifier = /^Identifier=(.+)$/m.exec(details)?.[1]?.trim();
|
|
146
|
+
if (!team || team !== teamIdentifier) fail("SIGN_DEV_ARTIFACT_TEAM_MISMATCH", `${rel}: expected TeamIdentifier=${teamIdentifier}, found ${team ?? "missing"}`);
|
|
147
|
+
if (!identifier || identifier !== record.identifier) fail("SIGN_DEV_ARTIFACT_IDENTIFIER_MISMATCH", `${rel}: expected Identifier=${record.identifier}, found ${identifier ?? "missing"}`);
|
|
148
|
+
const requirement = inspect("codesign", ["-d", "-r-", record.file]);
|
|
149
|
+
const designatedRequirement = /^designated => (.+)$/m.exec(requirement)?.[1]?.trim();
|
|
150
|
+
if (!designatedRequirement || /\bcdhash\b/.test(designatedRequirement)) fail("SIGN_DEV_ARTIFACT_UNSTABLE_REQUIREMENT", `${rel}: designated requirement ${JSON.stringify(designatedRequirement)} is not identity-based, so privacy grants would not survive a rebuild`);
|
|
151
|
+
return { path: rel, sha256Before: sha256Before.get(record.file), sha256After: record.after.sha256, identifier, teamIdentifier: team, designatedRequirement };
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Refresh the payload metadata: every recorded hash is recomputed (a signed bundle changes its tree digest too).
|
|
155
|
+
const payloadFiles = before.metadata.files.map((item) => {
|
|
156
|
+
const evidence = evidenceOf(contained(root, item.path, "payload metadata path"));
|
|
157
|
+
return { ...item, sha256: evidence.sha256, ...(item.sizeBytes !== undefined ? { sizeBytes: evidence.sizeBytes } : {}) };
|
|
158
|
+
});
|
|
159
|
+
const metadata = { ...before.metadata, files: payloadFiles, signing: { contract: MACOS_PORTABLE_SIGNING_CONTRACT, identity, teamIdentifier, signedPaths: files.map((file) => file.path) } };
|
|
160
|
+
const metadataBefore = evidenceOf(metadataFile).sha256;
|
|
161
|
+
const staging = `${metadataFile}.signing`;
|
|
162
|
+
rmSync(staging, { force: true }); // a stale file or planted link is removed itself, never written through
|
|
163
|
+
writeFileSync(staging, `${JSON.stringify(metadata, null, 2)}\n`, { flag: "wx" });
|
|
164
|
+
renameSync(staging, metadataFile);
|
|
165
|
+
verifyDevArtifactPayload(root, metadataFile);
|
|
166
|
+
|
|
167
|
+
const result = {
|
|
168
|
+
schema: DEV_ARTIFACT_EVIDENCE_SCHEMA, signingContract: MACOS_PORTABLE_SIGNING_CONTRACT, identity, teamIdentifier, notarized: false, built: false, published: false,
|
|
169
|
+
artifact: root, metadata: { path: toPosix(path.relative(root, metadataFile)), sha256Before: metadataBefore, sha256After: evidenceOf(metadataFile).sha256 }, files,
|
|
170
|
+
...(refreshOps.length ? { refresh: refreshOps } : {}),
|
|
171
|
+
};
|
|
172
|
+
const absoluteEvidence = path.resolve(evidencePath);
|
|
173
|
+
mkdirSync(path.dirname(absoluteEvidence), { recursive: true });
|
|
174
|
+
writeFileSync(absoluteEvidence, `${JSON.stringify(result, null, 2)}\n`);
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function main(args = process.argv.slice(2), options = {}) {
|
|
179
|
+
const take = (flag) => {
|
|
180
|
+
const index = args.indexOf(flag);
|
|
181
|
+
if (index < 0) return undefined;
|
|
182
|
+
const value = args[index + 1];
|
|
183
|
+
if (!value || value.startsWith("--")) fail("SIGN_DEV_ARTIFACT_USAGE", `${flag} requires a value`);
|
|
184
|
+
return value;
|
|
185
|
+
};
|
|
186
|
+
try {
|
|
187
|
+
const result = signDevArtifact({
|
|
188
|
+
artifact: take("--artifact"), manifestPath: take("--manifest"), metadataPath: take("--metadata"), evidencePath: take("--evidence"),
|
|
189
|
+
dryRun: args.includes("--dry-run"), ...options,
|
|
190
|
+
});
|
|
191
|
+
console.log(result.dryRun ? `dry-run: recorded hashes verified; would sign ${result.files.map((file) => file.path).join(", ")}` : `signed ${result.files.length} file(s) as ${result.teamIdentifier}: ${result.files.map((file) => `${file.path} (${file.identifier})`).join(", ")}`);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
console.error(`right-release sign-dev-artifact: ${error.code ? `[${error.code}] ` : ""}${error.message}`);
|
|
194
|
+
process.exitCode = 1;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) main();
|
package/stage-binary.mjs
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { statSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { replaceFileInPlace } from "./in-place-install.mjs";
|
|
4
|
+
import { resolveTargetRoot } from "./cargo-target.mjs";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A stable staged path for a development or QA binary.
|
|
8
|
+
*
|
|
9
|
+
* The build broker writes Cargo output to per-generation folders (`<managed>/rightkit-build-control/ws/<hash>/g/<gen>/...`)
|
|
10
|
+
* and `cargo-guard` uses a hashed `dev-targets/<project>-<hash>` folder. Those are build output only: their path is not
|
|
11
|
+
* stable, so NOTHING may run from them. macOS ties privacy grants to the path an app runs from, so a dev or QA binary is
|
|
12
|
+
* copied in place to `<root>/.rightkit-dev-bin/<triple-or-host>/<name>` (keeping `<name>.previous`) and run from there.
|
|
13
|
+
*/
|
|
14
|
+
export const DEV_STAGE_DIRECTORY = ".rightkit-dev-bin";
|
|
15
|
+
|
|
16
|
+
export function stagedBinaryPath({ root, name, triple = null }) {
|
|
17
|
+
if (typeof name !== "string" || !name || name !== path.basename(name) || name === "." || name === "..") throw new Error("staged binary name must be a bare file name");
|
|
18
|
+
if (triple !== null && !/^[A-Za-z0-9_.-]+$/.test(triple)) throw new Error("staged binary triple is not a safe path segment");
|
|
19
|
+
return path.join(path.resolve(root), DEV_STAGE_DIRECTORY, triple ?? "host", name);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Copy `source` to its stable staged path in place and return that path. Never launches or keeps anything per build. */
|
|
23
|
+
export function stageDevelopmentBinary({ root, source, name = path.basename(source), triple = null }) {
|
|
24
|
+
const from = path.resolve(root, source);
|
|
25
|
+
if (!statSync(from).isFile()) throw new Error(`staged binary source is not a file: ${from}`);
|
|
26
|
+
const destination = stagedBinaryPath({ root, name, triple });
|
|
27
|
+
replaceFileInPlace(from, destination);
|
|
28
|
+
return destination;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// The dev and test profiles are written to debug and bench to release (same mapping as development.mjs).
|
|
32
|
+
const profileDirectory = (profile) => (profile === "dev" || profile === "test" ? "debug" : profile === "bench" ? "release" : profile);
|
|
33
|
+
|
|
34
|
+
/** `{ source }` or `{ manifestPath, bin, targetTriple?, profile? }` (resolved through Cargo's own target directory, broker or not). */
|
|
35
|
+
export function resolveStageSource(entry, { root, resolveTarget = resolveTargetRoot, platform = process.platform } = {}) {
|
|
36
|
+
if (entry.source) return path.resolve(root, entry.source);
|
|
37
|
+
if (!entry.manifestPath || !entry.bin) throw new Error("development stage entry needs source, or manifestPath and bin");
|
|
38
|
+
const target = resolveTarget(path.resolve(root, entry.manifestPath));
|
|
39
|
+
const file = platform === "win32" ? `${entry.bin}.exe` : entry.bin;
|
|
40
|
+
return path.join(target, ...(entry.targetTriple ? [entry.targetTriple] : []), profileDirectory(entry.profile ?? "dev"), file);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function validateStageEntries(entries, label) {
|
|
44
|
+
if (entries === undefined) return [];
|
|
45
|
+
if (!Array.isArray(entries)) throw new Error(`${label} must be an array`);
|
|
46
|
+
for (const [index, entry] of entries.entries()) {
|
|
47
|
+
if (!entry || typeof entry !== "object" || (!entry.source && !(entry.manifestPath && entry.bin))) throw new Error(`${label}[${index}] needs source, or manifestPath and bin`);
|
|
48
|
+
if (entry.name !== undefined) stagedBinaryPath({ root: ".", name: entry.name });
|
|
49
|
+
if (entry.targetTriple !== undefined) stagedBinaryPath({ root: ".", name: "x", triple: entry.targetTriple });
|
|
50
|
+
}
|
|
51
|
+
return entries;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function stageDevelopmentBinaries(entries, { root, platform = process.platform, resolveTarget } = {}) {
|
|
55
|
+
return entries.map((entry) => {
|
|
56
|
+
const source = resolveStageSource(entry, { root, platform, resolveTarget });
|
|
57
|
+
const name = entry.name ?? path.basename(source);
|
|
58
|
+
return stageDevelopmentBinary({ root, source, name, triple: entry.targetTriple ?? null });
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `right-release stage-sidecar [--config right-release.config.mjs] [--target <triple>]` (bogusyogi/rightkit#345):
|
|
3
|
+
// stages the config's declarative `sidecars` into src-tauri/binaries with Tauri externalBin naming.
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { resolveTargetTriple, stageSidecars } from "./stage-sidecar.mjs";
|
|
7
|
+
|
|
8
|
+
export function parseStageSidecarArgs(argv) {
|
|
9
|
+
const opts = { config: "right-release.config.mjs", target: undefined, json: false };
|
|
10
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
11
|
+
const arg = argv[i];
|
|
12
|
+
if (arg === "--config" || arg === "--target") {
|
|
13
|
+
const value = argv[i + 1];
|
|
14
|
+
if (!value || value.startsWith("--")) throw new Error(`${arg} needs a value`);
|
|
15
|
+
opts[arg.slice(2)] = value;
|
|
16
|
+
i += 1;
|
|
17
|
+
} else if (arg === "--json") opts.json = true;
|
|
18
|
+
else throw new Error(`unknown stage-sidecar argument: ${arg}`);
|
|
19
|
+
}
|
|
20
|
+
return opts;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function main() {
|
|
24
|
+
const opts = parseStageSidecarArgs(process.argv.slice(2));
|
|
25
|
+
const configPath = path.resolve(opts.config);
|
|
26
|
+
const appRoot = path.dirname(configPath);
|
|
27
|
+
const loaded = await import(pathToFileURL(configPath).href);
|
|
28
|
+
const config = loaded.default ?? loaded.config ?? loaded;
|
|
29
|
+
if (config?.sidecars === undefined) throw new Error(`${opts.config} declares no sidecars`);
|
|
30
|
+
const target = resolveTargetTriple({ target: opts.target, cwd: appRoot });
|
|
31
|
+
const result = await stageSidecars({ appRoot, manifest: config.sidecars, target });
|
|
32
|
+
if (opts.json) console.log(JSON.stringify(result, null, 2));
|
|
33
|
+
else {
|
|
34
|
+
for (const file of result.files ?? []) console.log(`right-release stage-sidecar: staged ${typeof file === "string" ? file : file.path ?? JSON.stringify(file)}`);
|
|
35
|
+
console.log(`right-release stage-sidecar: externalBin ${JSON.stringify(result.externalBinNames ?? [])}; receipt ${result.receiptPath}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
|
40
|
+
main().catch((error) => { console.error(`right-release stage-sidecar: ${error.code ? `[${error.code}] ` : ""}${error.message}`); process.exitCode = 1; });
|
|
41
|
+
}
|