@rightkit/release 0.6.8 → 0.6.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -12
- package/admission-files.mjs +4 -1
- package/admission.mjs +2 -2
- package/advisory-exceptions.mjs +41 -5
- package/android-play-upload.mjs +16 -15
- package/app-check.mjs +105 -27
- package/assemble-app-command.mjs +151 -0
- package/assembled-app.mjs +445 -0
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/darwin/rightkit-swap.sign.json +6 -0
- package/bin/rightkit-gate.mjs +1608 -345
- package/broker-pins.mjs +152 -0
- package/build-launch.mjs +88 -0
- package/build-ownership.mjs +1231 -0
- package/build-release.mjs +82 -28
- package/cargo-contract.mjs +3 -1
- package/cargo-ledger.mjs +62 -0
- package/cargo-target-shim.mjs +238 -0
- package/class-a-launch-guard.mjs +126 -0
- package/cli/right-release.mjs +31 -5
- package/clippy-equivalence.mjs +202 -0
- package/clippy-route.mjs +543 -0
- package/codesign-shim.mjs +11 -1
- package/committed-source.mjs +39 -5
- package/crate-workspaces.mjs +87 -0
- package/create-mac-updater.mjs +14 -2
- package/deterministic-scans.mjs +997 -71
- package/development-signing.mjs +4 -1
- package/development.mjs +120 -21
- package/direct-receipt.mjs +135 -0
- package/door-policy.mjs +71 -6
- package/door.mjs +6 -6
- package/freshness-census.mjs +257 -0
- package/gate-executor.mjs +14 -2
- package/gate-stages.mjs +91 -15
- package/heavy-command.mjs +3 -2
- package/hook-retirement.mjs +65 -0
- package/init-command.mjs +1 -1
- package/install-app.mjs +11 -2
- package/install-assembled-app.mjs +423 -0
- package/inventory-check.mjs +11 -8
- package/ios-dev.mjs +70 -3
- package/ios-upload.mjs +20 -15
- package/linux-artifacts.mjs +93 -0
- package/local-publish.mjs +224 -53
- package/local-qualify.mjs +5 -3
- package/local-runtime.mjs +5 -4
- package/mac-package-recipe.mjs +4 -1
- package/managed-cargo.mjs +47 -5
- package/managed-root-guard.mjs +34 -0
- package/mirror-root-artifact.mjs +18 -5
- package/native-helpers.mjs +355 -0
- package/ownership-class-rules.mjs +1 -1
- package/ownership-evidence.mjs +37 -13
- package/ownership-hooks.mjs +20 -59
- package/ownership-lockfiles.mjs +14 -1
- package/ownership-policy-bundle.mjs +3 -2
- package/ownership-staged.mjs +2 -1
- package/ownership.mjs +24 -0
- package/package-recipe-targets.mjs +84 -10
- package/package.json +4 -3
- package/pipeline-admission.mjs +4 -4
- package/pipeline-receipts.mjs +302 -302
- package/preflight.mjs +27 -4
- package/project-type.mjs +2 -2
- package/provenance-verify.mjs +115 -0
- package/publish-cargo.mjs +12 -9
- package/qa-admission-policy.json +10 -1
- package/recipe-build.mjs +106 -14
- package/release-artifact-path.mjs +341 -0
- package/release-config.mjs +1 -1
- package/release-invocation.mjs +8 -1
- package/release-qa-command.mjs +25 -9
- package/release-source-brand.mjs +17 -0
- package/release-source-identity.mjs +171 -0
- package/release.mjs +59 -22
- package/rightkit-versions.json +73 -47
- package/runtime-budgets.mjs +6 -2
- package/scan-digest.mjs +71 -5
- package/sign-updater.mjs +6 -3
- package/snapshot-broker.mjs +172 -0
- package/snapshot-check.mjs +312 -0
- package/snapshot-git.mjs +166 -0
- package/snapshot-request.mjs +363 -0
- package/snapshot-root-config.mjs +76 -0
- package/snapshot.mjs +969 -0
- package/source-split.mjs +73 -0
- package/stage-commands.mjs +60 -0
- package/stage-ledger.mjs +204 -0
- package/stage-sidecar-command.mjs +5 -1
- package/stage-sidecar.mjs +157 -27
- package/swap-helper-bundle.mjs +5 -2
- package/test-command.mjs +17 -4
- package/unit-gate.mjs +301 -0
- package/web-build.mjs +40 -6
- package/win-package-recipe.mjs +20 -7
package/managed-cargo.mjs
CHANGED
|
@@ -1,11 +1,51 @@
|
|
|
1
1
|
import { discoverBrokerSocket, managedControlCandidates, resolveManagedRoots } from './managed-roots.mjs';
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { homedir, userInfo } from "node:os";
|
|
4
|
-
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { closeSync, existsSync, mkdirSync, openSync, readSync, rmSync, writeFileSync } from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { spawn as spawnProcess, spawnSync } from "./background-process.mjs";
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Linux, and any host that is neither Mac nor Windows, never uses the RustRight broker: WSL2, a VM, a cloud or CI runner, Linux hosted on
|
|
10
|
+
* the Dell (GATE-CONTRACT §1 "One entry"; joint plan step 4). Every managed-cargo helper follows this one policy, whatever control-root,
|
|
11
|
+
* socket or PATH entries the environment carries.
|
|
12
|
+
*/
|
|
13
|
+
export const directPlatform = (platform = process.platform) => platform !== "darwin" && platform !== "win32";
|
|
14
|
+
|
|
15
|
+
const MANAGED_AGENT_BIN = /(?:^|\/)(?:\.rightkit-managed|rightkitmanagedagent|rightkit\/build-control)\/agent-bin\/?$/i;
|
|
16
|
+
function fileHead(file) {
|
|
17
|
+
let fd = null;
|
|
18
|
+
try { fd = openSync(file, "r"); const b = Buffer.alloc(4096); const n = readSync(fd, b, 0, 4096, 0); return b.subarray(0, n).toString("latin1"); }
|
|
19
|
+
catch { return ""; } finally { if (fd !== null) try { closeSync(fd); } catch { /* closed */ } }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* On a direct (Linux) host: PATH without the directories whose `cargo` is a RustRight shim (the managed agent-bin, a configured
|
|
24
|
+
* RIGHTKIT_MANAGED_AGENT_BIN, or a script that routes through the broker, read 4 KiB at most), and the first real cargo left on it.
|
|
25
|
+
* Plain `cargo` then can only be a real toolchain; with none, a cargo launch fails as missing, never through a shim.
|
|
26
|
+
*/
|
|
27
|
+
export function directCargoPath({ env = process.env, exists = existsSync, readHead = fileHead } = {}) {
|
|
28
|
+
const configured = env.RIGHTKIT_MANAGED_AGENT_BIN ? path.posix.resolve(env.RIGHTKIT_MANAGED_AGENT_BIN) : null;
|
|
29
|
+
const kept = []; const dropped = []; let cargo = null;
|
|
30
|
+
for (const dir of String(env.PATH ?? "").split(":").filter(Boolean)) {
|
|
31
|
+
const candidate = path.posix.join(dir, "cargo");
|
|
32
|
+
const isShim = exists(candidate) && (MANAGED_AGENT_BIN.test(dir) || (configured && path.posix.resolve(dir) === configured) || /cargo-guard\.mjs|rustright/i.test(readHead(candidate)));
|
|
33
|
+
if (isShim) { dropped.push(dir); continue; }
|
|
34
|
+
kept.push(dir);
|
|
35
|
+
if (!cargo && exists(candidate)) cargo = candidate;
|
|
36
|
+
}
|
|
37
|
+
return { path: kept.join(":"), dropped, cargo };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** `env` for a direct host: PATH without RustRight shim directories. Unchanged on Mac and Windows. */
|
|
41
|
+
export function directCargoEnv(env = process.env, platform = process.platform, exists = existsSync) {
|
|
42
|
+
if (!directPlatform(platform)) return env;
|
|
43
|
+
const direct = directCargoPath({ env, exists });
|
|
44
|
+
return direct.dropped.length ? { ...env, PATH: direct.path } : env;
|
|
45
|
+
}
|
|
46
|
+
|
|
8
47
|
export function managedRuntimeClient(env = process.env, platform = process.platform, exists = existsSync) {
|
|
48
|
+
if (directPlatform(platform)) return undefined;
|
|
9
49
|
const roots = managedControlCandidates({ env, platform });
|
|
10
50
|
const paths = platform === 'win32' ? path.win32 : path.posix;
|
|
11
51
|
// runtime-current ships the native `rustright` client.
|
|
@@ -54,7 +94,8 @@ export function acquireManagedRunLease({ command, env = process.env, platform =
|
|
|
54
94
|
return { path: file, release() { rmSync(file, { force: true }); } };
|
|
55
95
|
}
|
|
56
96
|
|
|
57
|
-
export function managedHost(env) {
|
|
97
|
+
export function managedHost(env, platform = process.platform) {
|
|
98
|
+
if (directPlatform(platform)) return false; // Linux is never broker-managed (directPlatform), whatever the env names
|
|
58
99
|
return Boolean(env.RIGHTKIT_BUILD_BROKER_SOCKET)
|
|
59
100
|
|| /(?:^|[;:])[^;:]*[/\\](?:\.rightkit-managed|rightkitmanagedagent|rightkit[/\\]build-control)[/\\]agent-bin(?:[;:]|$)/i.test(String(env.PATH ?? ""));
|
|
60
101
|
}
|
|
@@ -75,9 +116,10 @@ export function runCargoSync(args, options = {}, { platform = process.platform,
|
|
|
75
116
|
}
|
|
76
117
|
// The Mac never falls back to direct Cargo, except on GitHub Actions runners, which have no
|
|
77
118
|
// RightKit runtime and use the toolchain's cargo (release-candidate signing jobs).
|
|
78
|
-
const hostedCi = env.GITHUB_ACTIONS === 'true' && !managedHost(env);
|
|
79
|
-
if ((platform === 'darwin' && !hostedCi) || (managedHost(env) && platform === 'win32')) return unavailable('installed RightKit runtime client is unavailable for managed Cargo metadata');
|
|
80
|
-
|
|
119
|
+
const hostedCi = env.GITHUB_ACTIONS === 'true' && !managedHost(env, platform);
|
|
120
|
+
if ((platform === 'darwin' && !hostedCi) || (managedHost(env, platform) && platform === 'win32')) return unavailable('installed RightKit runtime client is unavailable for managed Cargo metadata');
|
|
121
|
+
// A direct (Linux) host runs the real toolchain: RustRight shim directories are taken off PATH first.
|
|
122
|
+
return spawn("cargo", args, directPlatform(platform) ? { ...options, env: directCargoEnv(env, platform, exists) } : options);
|
|
81
123
|
}
|
|
82
124
|
|
|
83
125
|
function unavailable(message) {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// The managed-root guard (item 5, signed item5-sage-decisions-2.md D4 rule 1): the gate never creates the host's managed root, and on
|
|
2
|
+
// the Mac never accepts one on the boot volume. It lives here, not in snapshot.mjs, because build-ownership.mjs guards the root as the
|
|
3
|
+
// FIRST action of an acquisition and snapshot.mjs already imports build-ownership.mjs. snapshot.mjs re-exports both names.
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
|
|
6
|
+
export class SnapshotError extends Error {
|
|
7
|
+
/** `code` is "mismatch" (the snapshot is not the commit's tree), "busy" (another sync holds it), "refused" (the commit cannot be
|
|
8
|
+
* materialised exactly, or the root is not one a snapshot may live in: a gitlink, an unsafe path, a link out of the snapshot, a
|
|
9
|
+
* missing managed root, an open-mode root, an unknown revision) or "error" (could not run). `reason`, when present, names the
|
|
10
|
+
* refusal: "gitlink", "unknown-revision", "managed-root-missing", "managed-root-on-boot-volume", "root-open-mode",
|
|
11
|
+
* "root-not-owned", "root-parent-missing". */
|
|
12
|
+
constructor(code, message, reason) { super(message); this.code = code; if (reason) this.reason = reason; }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const lstat = (p) => { try { return fs.lstatSync(p, { bigint: true }); } catch (e) { if (e.code === "ENOENT" || e.code === "ENOTDIR") return null; throw e; } };
|
|
16
|
+
const deviceOf = (p) => fs.statSync(p, { bigint: true }).dev;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The managed root must already exist (the gate never creates it) and, on the Mac, live on another device than `/`: with the volume
|
|
20
|
+
* unmounted, `/Volumes/D/.rightkit-managed` made on the boot volume (by anything) would otherwise hold the snapshots. Volume identity
|
|
21
|
+
* and the disk floor for the broker's own writes are the broker's (BC B16). Throws SnapshotError("refused") with a named reason.
|
|
22
|
+
*/
|
|
23
|
+
export function requireManagedRoot(managedRoot, { platform = process.platform, device = deviceOf } = {}) {
|
|
24
|
+
const st = lstat(managedRoot);
|
|
25
|
+
if (!st) throw new SnapshotError("refused", `the managed root ${managedRoot} does not exist: the gate never creates it (the volume may be unmounted); nothing was written`, "managed-root-missing");
|
|
26
|
+
if (!st.isDirectory()) throw new SnapshotError("refused", `the managed root ${managedRoot} is not a plain directory (a link is never followed)`, "managed-root-missing");
|
|
27
|
+
if (platform === "darwin") {
|
|
28
|
+
let here; let boot;
|
|
29
|
+
try { here = device(managedRoot); boot = device("/"); }
|
|
30
|
+
catch (e) { throw new SnapshotError("error", `cannot read the device of the managed root ${managedRoot} or of /: ${e.message}`); }
|
|
31
|
+
if (here === boot) throw new SnapshotError("refused", `the managed root ${managedRoot} is on the same device as / (the boot volume): a snapshot there would not be on the managed volume (is the volume mounted?)`, "managed-root-on-boot-volume");
|
|
32
|
+
}
|
|
33
|
+
return managedRoot;
|
|
34
|
+
}
|
package/mirror-root-artifact.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { copyFileSync, lstatSync, mkdirSync, readdirSync, realpathSync, renameSy
|
|
|
11
11
|
import { execFileSync } from "./background-process.mjs";
|
|
12
12
|
import path from "node:path";
|
|
13
13
|
import { importReleaseConfig } from "./release-config.mjs";
|
|
14
|
+
import { ArtifactPathError, isSplitArtifactEnv, loadSplitArtifactContext, resolveArtifactPath } from "./release-artifact-path.mjs";
|
|
14
15
|
|
|
15
16
|
const args = parseArgs(process.argv.slice(2));
|
|
16
17
|
const usage = "Usage: right-release mirror-root-artifact (--file <artifact> --package-root <dir> [--name <filename>] | --config <file> --platform mac|win [--package-root <dir>])";
|
|
@@ -52,15 +53,27 @@ if (args.config) {
|
|
|
52
53
|
|
|
53
54
|
const packageRoot = realpathSync(path.resolve(rootArg));
|
|
54
55
|
const managedOutput = path.resolve(process.env.RIGHT_RELEASE_ARTIFACT_DIR);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
// A door split (row 7.04c): the package root is the snapshot, which has no git directory; the repositories the output must stay out of are the
|
|
57
|
+
// checkout and the snapshot, and a target-prefixed source is the bound Cargo target's (read in place, never the checkout's).
|
|
58
|
+
const splitMirror = isSplitArtifactEnv(process.env);
|
|
59
|
+
const artifactPaths = await loadSplitArtifactContext({ cwd: packageRoot });
|
|
60
|
+
const repoRoots = splitMirror ? [path.resolve(process.env.RK_GATE_CHECKOUT), packageRoot] : [path.resolve(git(packageRoot, "rev-parse", "--show-toplevel"))];
|
|
61
|
+
for (const repoRoot of repoRoots) {
|
|
62
|
+
if (inside(managedOutput, repoRoot)) {
|
|
63
|
+
console.error(`right-release mirror-root-artifact: managed output must be outside repository: ${managedOutput}`);
|
|
64
|
+
process.exit(2);
|
|
65
|
+
}
|
|
59
66
|
}
|
|
60
67
|
const safeName = (name) => name === path.basename(name) && /^[A-Za-z0-9][A-Za-z0-9._+-]{0,159}$/.test(name);
|
|
61
68
|
|
|
62
69
|
for (const entry of entries) {
|
|
63
|
-
|
|
70
|
+
let source;
|
|
71
|
+
try { source = splitMirror ? resolveArtifactPath(entry.file, artifactPaths) : path.resolve(packageRoot, entry.file); }
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (!(error instanceof ArtifactPathError)) throw error;
|
|
74
|
+
console.error(`right-release mirror-root-artifact: ${error.message}`);
|
|
75
|
+
process.exit(2);
|
|
76
|
+
}
|
|
64
77
|
const name = entry.name || path.basename(source);
|
|
65
78
|
if (!safeName(name)) {
|
|
66
79
|
console.error(`right-release mirror-root-artifact: unsafe artifact name: ${name}`);
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
// Declared native Swift helpers embedded in a macOS .app (shared by the dev lane and the release lane).
|
|
2
|
+
//
|
|
3
|
+
// nativeHelpers: [{
|
|
4
|
+
// name: "menubar", // unique label (logs, receipts)
|
|
5
|
+
// package: "macos-native/Pkg" // app-relative SwiftPM package dir ...
|
|
6
|
+
// // sharedPackage: "cloudkit", // ... or a RightKit shared Swift package key (exactly one of the two)
|
|
7
|
+
// product: "MenuBar", // SwiftPM executable product
|
|
8
|
+
// kind: "app" | "executable",
|
|
9
|
+
// embed: "Contents/Library/LoginItems/MenuBar.app", // path inside the .app
|
|
10
|
+
// universal: true, // arm64 + x86_64 (required, explicit)
|
|
11
|
+
// entitlements: "src-tauri/entitlements-x.plist", // optional, app-relative
|
|
12
|
+
// // kind "app" only (wrap the SwiftPM executable as a helper .app):
|
|
13
|
+
// executable: "MenuBar", // CFBundleExecutable / file in Contents/MacOS (default: product)
|
|
14
|
+
// info: { bundleId, name?, displayName?, uiElement?, minimumSystemVersion? }, // Info.plist fields
|
|
15
|
+
// resources: [{ from: "src-tauri/icons/tray", to: "tray" }], // copied into Contents/Resources
|
|
16
|
+
// appIntents: { moduleName, deploymentTarget? }, // appintentsmetadataprocessor step
|
|
17
|
+
// provisioningProfile: "src-tauri/provisioning/X.provisionprofile", // -> Contents/embedded.provisionprofile
|
|
18
|
+
// }]
|
|
19
|
+
//
|
|
20
|
+
// Build: `swift build -c <release|debug> --package-path <pkg> --product <product> --arch <a> [--arch <b>]` (SwiftPM, the same
|
|
21
|
+
// tool swift-package / assemble-app use; one explicit --arch per slice so the output lands in SwiftPM's xcbuild layout for
|
|
22
|
+
// both thin and universal builds), then `--show-bin-path` to find the product. Embedded BEFORE the app is signed. Signing is
|
|
23
|
+
// inside-out: release.mjs signs the helpers through sign.nestedCode (macPackage expansion derives it), the dev signer signs
|
|
24
|
+
// nested bundles itself, and signNativeHelpers below does the same for any other caller. Every external effect is injectable.
|
|
25
|
+
import { createHash } from "node:crypto";
|
|
26
|
+
import { buildLaunch } from "./build-launch.mjs";
|
|
27
|
+
import * as nodeFs from "node:fs";
|
|
28
|
+
import os from "node:os";
|
|
29
|
+
import path from "node:path";
|
|
30
|
+
import { fileURLToPath } from "node:url";
|
|
31
|
+
import { spawnSync as nodeSpawnSync } from "./background-process.mjs";
|
|
32
|
+
import { signMacosEntries } from "./mac-sign-core.mjs";
|
|
33
|
+
import { discoverNestedCode } from "./swift-mac-app.mjs";
|
|
34
|
+
|
|
35
|
+
export class NativeHelpersError extends Error {
|
|
36
|
+
constructor(code, message, details = {}) { super(message); this.name = "NativeHelpersError"; this.code = code; this.details = details; }
|
|
37
|
+
}
|
|
38
|
+
const fail = (code, message, details) => { throw new NativeHelpersError(code, message, details); };
|
|
39
|
+
const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
40
|
+
|
|
41
|
+
export const NATIVE_HELPER_KEYS = Object.freeze(["name", "package", "sharedPackage", "product", "kind", "embed", "universal", "entitlements", "executable", "info", "resources", "appIntents", "provisioningProfile"]);
|
|
42
|
+
const INFO_KEYS = ["bundleId", "name", "displayName", "uiElement", "minimumSystemVersion"];
|
|
43
|
+
const APP_INTENTS_KEYS = ["moduleName", "deploymentTarget"];
|
|
44
|
+
const RESOURCE_KEYS = ["from", "to"];
|
|
45
|
+
/** RightKit shared Swift packages, keyed, relative to the RightKit checkout root (env RIGHTKIT_SWIFT_ROOT overrides the root). */
|
|
46
|
+
export const SHARED_SWIFT_PACKAGES = Object.freeze({ cloudkit: "swift/RightKitCloudKitHelper" });
|
|
47
|
+
/** Where a helper may be embedded: only roots the inside-out signing discovery walks. */
|
|
48
|
+
const EMBED_ROOTS = ["Contents/Library/", "Contents/Helpers/", "Contents/XPCServices/", "Contents/PlugIns/", "Contents/MacOS/"];
|
|
49
|
+
const NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
50
|
+
const PRODUCT = /^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$/;
|
|
51
|
+
const FLAT = /^[A-Za-z0-9_][A-Za-z0-9_. -]{0,127}$/;
|
|
52
|
+
const VERSION = /^\d+(\.\d+){0,2}$/;
|
|
53
|
+
const BUNDLE_ID = /^[A-Za-z0-9.-]+\.[A-Za-z0-9.-]+$/;
|
|
54
|
+
const ARCHS = ["arm64", "x86_64"];
|
|
55
|
+
|
|
56
|
+
function relative(value, label, errors, { ext } = {}) {
|
|
57
|
+
if (typeof value !== "string" || !value.trim()) { errors.push(`${label} must be a non-empty relative path`); return undefined; }
|
|
58
|
+
if (value.includes("\0") || value.includes("\\") || path.isAbsolute(value) || /^[A-Za-z]:/.test(value)) { errors.push(`${label} must be a relative path with forward slashes: ${JSON.stringify(value)}`); return undefined; }
|
|
59
|
+
const segments = value.split("/");
|
|
60
|
+
if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) { errors.push(`${label} must not contain empty, '.' or '..' segments or escape its root: ${JSON.stringify(value)}`); return undefined; }
|
|
61
|
+
if (ext && !ext.test(value)) { errors.push(`${label} must match ${ext}: ${JSON.stringify(value)}`); return undefined; }
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
const unknown = (object, allowed, label, errors) => { for (const key of Object.keys(object)) if (!allowed.includes(key)) errors.push(`${label}: unknown key ${JSON.stringify(key)} (allowed: ${allowed.join(", ")})`); };
|
|
65
|
+
|
|
66
|
+
/** Strictly validate and normalize a `nativeHelpers` list. Every problem is reported at once (NATIVE_HELPERS_INVALID). */
|
|
67
|
+
export function validateNativeHelpers(list, { label = "nativeHelpers" } = {}) {
|
|
68
|
+
const errors = [];
|
|
69
|
+
if (!Array.isArray(list) || !list.length) fail("NATIVE_HELPERS_INVALID", `${label} must be a non-empty array`);
|
|
70
|
+
const out = [];
|
|
71
|
+
list.forEach((raw, index) => {
|
|
72
|
+
const at = `${label}[${index}]`;
|
|
73
|
+
if (!isObject(raw)) { errors.push(`${at} must be an object`); return; }
|
|
74
|
+
unknown(raw, NATIVE_HELPER_KEYS, at, errors);
|
|
75
|
+
const helper = {};
|
|
76
|
+
if (typeof raw.name !== "string" || !NAME.test(raw.name)) errors.push(`${at}.name must be 1-64 characters of letters, digits, '.', '_' and '-'`); else helper.name = raw.name;
|
|
77
|
+
if ((raw.package === undefined) === (raw.sharedPackage === undefined)) errors.push(`${at} needs exactly one of package or sharedPackage`);
|
|
78
|
+
else if (raw.package !== undefined) { const value = relative(raw.package, `${at}.package`, errors); if (value) helper.package = value; }
|
|
79
|
+
else if (typeof raw.sharedPackage !== "string" || !Object.hasOwn(SHARED_SWIFT_PACKAGES, raw.sharedPackage)) errors.push(`${at}.sharedPackage must be one of ${Object.keys(SHARED_SWIFT_PACKAGES).join(", ")}`);
|
|
80
|
+
else helper.sharedPackage = raw.sharedPackage;
|
|
81
|
+
if (typeof raw.product !== "string" || !PRODUCT.test(raw.product)) errors.push(`${at}.product must be a SwiftPM product name`); else helper.product = raw.product;
|
|
82
|
+
if (raw.kind !== "app" && raw.kind !== "executable") errors.push(`${at}.kind must be "app" or "executable"`); else helper.kind = raw.kind;
|
|
83
|
+
if (typeof raw.universal !== "boolean") errors.push(`${at}.universal is required and must be true or false`); else helper.universal = raw.universal;
|
|
84
|
+
const embed = relative(raw.embed, `${at}.embed`, errors);
|
|
85
|
+
if (embed) {
|
|
86
|
+
if (!EMBED_ROOTS.some((root) => embed.startsWith(root)) || embed.split("/").length < 3) errors.push(`${at}.embed must be inside the .app under ${EMBED_ROOTS.join(" | ")}: ${embed}`);
|
|
87
|
+
else if (helper.kind === "app" && (!embed.endsWith(".app") || embed.startsWith("Contents/MacOS/"))) errors.push(`${at}.embed for kind "app" must be a <Name>.app below Contents/Library, Helpers, XPCServices or PlugIns: ${embed}`);
|
|
88
|
+
else if (helper.kind === "executable" && (embed.endsWith(".app") || (embed.startsWith("Contents/MacOS/") && embed.split("/").length !== 3))) errors.push(`${at}.embed for kind "executable" must be a file (Contents/MacOS/<name> or a path below another allowed root): ${embed}`);
|
|
89
|
+
else helper.embed = embed;
|
|
90
|
+
}
|
|
91
|
+
if (raw.entitlements !== undefined) { const value = relative(raw.entitlements, `${at}.entitlements`, errors, { ext: /\.plist$/i }); if (value) helper.entitlements = value; }
|
|
92
|
+
if (raw.executable !== undefined) {
|
|
93
|
+
if (raw.kind !== "app") errors.push(`${at}.executable applies only to kind "app"`);
|
|
94
|
+
else if (typeof raw.executable !== "string" || !FLAT.test(raw.executable) || raw.executable.includes("/")) errors.push(`${at}.executable must be a flat file name`);
|
|
95
|
+
else helper.executable = raw.executable;
|
|
96
|
+
}
|
|
97
|
+
if (raw.kind === "app") {
|
|
98
|
+
if (!isObject(raw.info)) errors.push(`${at}.info is required for kind "app" ({ bundleId, name?, displayName?, uiElement?, minimumSystemVersion? })`);
|
|
99
|
+
else {
|
|
100
|
+
unknown(raw.info, INFO_KEYS, `${at}.info`, errors);
|
|
101
|
+
const info = {};
|
|
102
|
+
if (typeof raw.info.bundleId !== "string" || !BUNDLE_ID.test(raw.info.bundleId)) errors.push(`${at}.info.bundleId must be reverse-DNS`); else info.bundleId = raw.info.bundleId;
|
|
103
|
+
for (const key of ["name", "displayName"]) if (raw.info[key] !== undefined) { if (typeof raw.info[key] !== "string" || !FLAT.test(raw.info[key])) errors.push(`${at}.info.${key} must be a simple name`); else info[key] = raw.info[key]; }
|
|
104
|
+
if (raw.info.uiElement !== undefined) { if (typeof raw.info.uiElement !== "boolean") errors.push(`${at}.info.uiElement must be a boolean`); else info.uiElement = raw.info.uiElement; }
|
|
105
|
+
if (raw.info.minimumSystemVersion !== undefined) { if (typeof raw.info.minimumSystemVersion !== "string" || !VERSION.test(raw.info.minimumSystemVersion)) errors.push(`${at}.info.minimumSystemVersion must be 1-3 numeric parts`); else info.minimumSystemVersion = raw.info.minimumSystemVersion; }
|
|
106
|
+
helper.info = info;
|
|
107
|
+
}
|
|
108
|
+
if (raw.resources !== undefined) {
|
|
109
|
+
if (!Array.isArray(raw.resources)) errors.push(`${at}.resources must be an array of { from, to }`);
|
|
110
|
+
else helper.resources = raw.resources.map((item, i) => {
|
|
111
|
+
const where = `${at}.resources[${i}]`;
|
|
112
|
+
if (!isObject(item)) { errors.push(`${where} must be an object`); return undefined; }
|
|
113
|
+
unknown(item, RESOURCE_KEYS, where, errors);
|
|
114
|
+
const from = relative(item.from, `${where}.from`, errors);
|
|
115
|
+
const to = relative(item.to, `${where}.to`, errors);
|
|
116
|
+
return from && to ? { from, to } : undefined;
|
|
117
|
+
}).filter(Boolean);
|
|
118
|
+
}
|
|
119
|
+
if (raw.appIntents !== undefined) {
|
|
120
|
+
if (!isObject(raw.appIntents)) errors.push(`${at}.appIntents must be { moduleName, deploymentTarget? }`);
|
|
121
|
+
else {
|
|
122
|
+
unknown(raw.appIntents, APP_INTENTS_KEYS, `${at}.appIntents`, errors);
|
|
123
|
+
if (typeof raw.appIntents.moduleName !== "string" || !PRODUCT.test(raw.appIntents.moduleName)) errors.push(`${at}.appIntents.moduleName must be a Swift module name`);
|
|
124
|
+
if (raw.appIntents.deploymentTarget !== undefined && (typeof raw.appIntents.deploymentTarget !== "string" || !VERSION.test(raw.appIntents.deploymentTarget))) errors.push(`${at}.appIntents.deploymentTarget must be 1-3 numeric parts`);
|
|
125
|
+
helper.appIntents = { moduleName: raw.appIntents.moduleName, deploymentTarget: raw.appIntents.deploymentTarget ?? "14.0" };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (raw.provisioningProfile !== undefined) { const value = relative(raw.provisioningProfile, `${at}.provisioningProfile`, errors, { ext: /\.(provisionprofile|mobileprovision)$/ }); if (value) helper.provisioningProfile = value; }
|
|
129
|
+
} else if (raw.kind === "executable") {
|
|
130
|
+
for (const key of ["info", "resources", "appIntents", "provisioningProfile"]) if (raw[key] !== undefined) errors.push(`${at}.${key} applies only to kind "app"`);
|
|
131
|
+
}
|
|
132
|
+
out.push(helper);
|
|
133
|
+
});
|
|
134
|
+
const names = new Set();
|
|
135
|
+
const embeds = [];
|
|
136
|
+
for (const helper of out) {
|
|
137
|
+
if (helper.name) { if (names.has(helper.name)) errors.push(`${label}: duplicate name ${helper.name}`); names.add(helper.name); }
|
|
138
|
+
if (helper.embed) {
|
|
139
|
+
const clash = embeds.find((other) => other === helper.embed || other.startsWith(`${helper.embed}/`) || helper.embed.startsWith(`${other}/`));
|
|
140
|
+
if (clash) errors.push(`${label}: embed paths must be unique and not nested in each other (${clash} vs ${helper.embed})`);
|
|
141
|
+
embeds.push(helper.embed);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (errors.length) fail("NATIVE_HELPERS_INVALID", `invalid ${label}:\n${errors.map((line) => `- ${line}`).join("\n")}`, { errors });
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Release signing config derived from the list: `sign.nestedCode[embed]` per helper (hardened runtime is implicit, entitlements per helper). */
|
|
149
|
+
export function nativeHelperNestedCode(helpers) {
|
|
150
|
+
return Object.fromEntries(helpers.map((helper) => [helper.embed, helper.entitlements ? { entitlements: helper.entitlements } : {}]));
|
|
151
|
+
}
|
|
152
|
+
/** Dev signer map `{ <bundle-relative embed>: <entitlements plist> }` for helpers that declare entitlements (merge into install.entitlements). */
|
|
153
|
+
export function nativeHelperEntitlementMap(helpers) {
|
|
154
|
+
return Object.fromEntries(helpers.filter((helper) => helper.entitlements).map((helper) => [helper.embed, helper.entitlements]));
|
|
155
|
+
}
|
|
156
|
+
/** The shared list from a release config: `targets.mac.macPackage.nativeHelpers` (undefined when none is declared). */
|
|
157
|
+
export function nativeHelpersFromConfig(config) {
|
|
158
|
+
const raw = config?.targets?.mac?.macPackage?.nativeHelpers;
|
|
159
|
+
return raw === undefined ? undefined : validateNativeHelpers(raw, { label: "targets.mac.macPackage.nativeHelpers" });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const esc = (value) => String(value).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
163
|
+
/** Info.plist for a wrapped helper .app (same fields the helper bundles always carried). */
|
|
164
|
+
export function helperInfoPlist({ helper, executable, version }) {
|
|
165
|
+
const info = helper.info;
|
|
166
|
+
const rows = [
|
|
167
|
+
["CFBundleExecutable", `<string>${esc(executable)}</string>`],
|
|
168
|
+
["CFBundleIdentifier", `<string>${esc(info.bundleId)}</string>`],
|
|
169
|
+
["CFBundleName", `<string>${esc(info.name ?? helper.product)}</string>`],
|
|
170
|
+
...(info.displayName ? [["CFBundleDisplayName", `<string>${esc(info.displayName)}</string>`]] : []),
|
|
171
|
+
["CFBundlePackageType", "<string>APPL</string>"],
|
|
172
|
+
["CFBundleShortVersionString", `<string>${esc(version)}</string>`],
|
|
173
|
+
["CFBundleVersion", `<string>${esc(version)}</string>`],
|
|
174
|
+
...(info.uiElement ? [["LSUIElement", "<true/>"]] : []),
|
|
175
|
+
["LSMinimumSystemVersion", `<string>${esc(info.minimumSystemVersion ?? "14.0")}</string>`],
|
|
176
|
+
];
|
|
177
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0"><dict>\n${rows.map(([key, value]) => ` <key>${key}</key>${value}`).join("\n")}\n</dict></plist>\n`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const sha256 = (fs, file) => createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
|
181
|
+
const tail = (text, lines = 15) => String(text ?? "").split(/\r?\n/).slice(-lines).join("\n");
|
|
182
|
+
const archSet = (text) => String(text ?? "").trim().split(/\s+/).filter(Boolean).sort();
|
|
183
|
+
|
|
184
|
+
function findFile(fs, root, predicate) {
|
|
185
|
+
let entries;
|
|
186
|
+
try { entries = fs.readdirSync(root, { withFileTypes: true }); } catch { return null; }
|
|
187
|
+
for (const entry of entries) {
|
|
188
|
+
const candidate = path.join(root, entry.name);
|
|
189
|
+
if (entry.isDirectory()) { const found = findFile(fs, candidate, predicate); if (found) return found; }
|
|
190
|
+
else if (predicate(candidate.split(path.sep).join("/"))) return candidate;
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Build every helper with SwiftPM and embed it into `app` (before signing). Strictly in declared order; the first failure throws.
|
|
197
|
+
* options: appRoot (absolute), app (absolute .app), helpers (validated or raw), version (required with a kind "app" helper),
|
|
198
|
+
* forceArch ("arm64"|"x86_64": build only that slice for every helper, e.g. a dev build or a thin release triple),
|
|
199
|
+
* configuration ("release"|"debug"), env, spawn, fs, log, replaceExisting (default false: an existing embed path is an error),
|
|
200
|
+
* sharedRoot, hostArch, tmpdir. Without forceArch a universal helper gets both slices and a thin one the host arch.
|
|
201
|
+
* Returns { helpers: [{ name, embed, path, source, archs, sha256 }], events: [...] }.
|
|
202
|
+
*/
|
|
203
|
+
export function buildAndEmbedNativeHelpers({ appRoot, app, helpers: rawHelpers, version, forceArch, configuration = "release", env = process.env, spawn = nodeSpawnSync, fs = nodeFs, log = () => {}, replaceExisting = false, sharedRoot, hostArch = process.arch === "arm64" ? "arm64" : "x86_64", tmpdir = os.tmpdir(), packageOwner = (pkg) => { const result = nodeSpawnSync("git", ["-C", pkg, "rev-parse", "--show-toplevel"], { encoding: "utf8", windowsHide: true }); return result.status === 0 ? String(result.stdout).trim() : null; } } = {}) {
|
|
204
|
+
if (typeof appRoot !== "string" || !path.isAbsolute(appRoot)) fail("NATIVE_HELPERS_INVALID", "appRoot must be an absolute path");
|
|
205
|
+
if (typeof app !== "string" || !path.isAbsolute(app) || !app.replace(/\/+$/, "").endsWith(".app")) fail("NATIVE_HELPERS_INVALID", "app must be the absolute path of the .app bundle");
|
|
206
|
+
if (!["release", "debug"].includes(configuration)) fail("NATIVE_HELPERS_INVALID", "configuration must be release or debug");
|
|
207
|
+
if (forceArch !== undefined && !ARCHS.includes(forceArch)) fail("NATIVE_HELPERS_INVALID", `forceArch must be one of ${ARCHS.join(", ")}`);
|
|
208
|
+
const helpers = validateNativeHelpers(rawHelpers);
|
|
209
|
+
if (helpers.some((helper) => helper.kind === "app") && (typeof version !== "string" || !VERSION.test(version))) fail("NATIVE_HELPERS_INVALID", "a kind \"app\" helper needs the app version (1-3 numeric parts) for its Info.plist");
|
|
210
|
+
if (!fs.existsSync(path.join(app, "Contents", "Info.plist"))) fail("NATIVE_HELPERS_INPUT_MISSING", `${app} is not a built .app (no Contents/Info.plist); helpers are embedded after the app build`);
|
|
211
|
+
const root = sharedRoot ?? env.RIGHTKIT_SWIFT_ROOT ?? fileURLToPath(new URL("../../", import.meta.url));
|
|
212
|
+
const events = [];
|
|
213
|
+
const results = [];
|
|
214
|
+
const run = (what, cmd, args, { capture = false, cwd = appRoot } = {}) => {
|
|
215
|
+
log(`native-helpers: ${what}: ${cmd} ${args.join(" ")}`);
|
|
216
|
+
const launch = cmd === "swift" ? buildLaunch(cmd, args, { cwd, env, exists: fs.existsSync }) : { command: cmd, args };
|
|
217
|
+
const result = spawn(launch.command, launch.args, { cwd, env, ...(capture ? { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] } : { stdio: "inherit" }), windowsHide: true });
|
|
218
|
+
if (!result || result.error || result.status !== 0) fail("NATIVE_HELPERS_STEP_FAILED", `native helper ${what} failed (${result?.error?.message ?? `exit ${result?.status ?? result?.signal ?? "unknown"}`})${capture ? `: ${tail(result?.stderr)}` : ""}`, { cmd, status: result?.status ?? null });
|
|
219
|
+
return result;
|
|
220
|
+
};
|
|
221
|
+
for (const helper of helpers) {
|
|
222
|
+
const pkgDir = helper.package ? path.resolve(appRoot, helper.package) : path.resolve(root, SHARED_SWIFT_PACKAGES[helper.sharedPackage]);
|
|
223
|
+
if (helper.package && path.relative(appRoot, pkgDir).startsWith("..")) fail("NATIVE_HELPERS_INVALID", `${helper.name}: package escapes the app root`);
|
|
224
|
+
if (!fs.existsSync(path.join(pkgDir, "Package.swift"))) fail("NATIVE_HELPERS_INPUT_MISSING", `${helper.name}: no Package.swift in ${pkgDir}${helper.sharedPackage ? " (set RIGHTKIT_SWIFT_ROOT to the RightKit checkout)" : ""}`);
|
|
225
|
+
const entitlements = helper.entitlements ? path.resolve(appRoot, helper.entitlements) : undefined;
|
|
226
|
+
if (entitlements && !fs.existsSync(entitlements)) fail("NATIVE_HELPERS_INPUT_MISSING", `${helper.name}: entitlements file not found: ${entitlements}`);
|
|
227
|
+
const archs = forceArch ? [forceArch] : helper.universal ? [...ARCHS] : [hostArch];
|
|
228
|
+
// Each Swift job is admitted against the checkout that owns its package, including shared RightKit helpers.
|
|
229
|
+
const packageRoot = packageOwner(pkgDir) || appRoot;
|
|
230
|
+
const relativePackage = path.relative(packageRoot, pkgDir);
|
|
231
|
+
const packageArg = relativePackage === "" ? "." : relativePackage;
|
|
232
|
+
const swiftArgs = ["build", "-c", configuration, "--package-path", packageArg, "--product", helper.product, ...archs.flatMap((arch) => ["--arch", arch])];
|
|
233
|
+
run(`build ${helper.name}`, "swift", swiftArgs, { cwd: packageRoot });
|
|
234
|
+
events.push(`build:${helper.name}`);
|
|
235
|
+
const binDir = String(run(`bin path ${helper.name}`, "swift", [...swiftArgs, "--show-bin-path"], { capture: true, cwd: packageRoot }).stdout ?? "").trim().split(/\r?\n/).pop();
|
|
236
|
+
if (!binDir || !path.isAbsolute(binDir)) fail("NATIVE_HELPERS_STEP_FAILED", `${helper.name}: swift build --show-bin-path returned no absolute path`);
|
|
237
|
+
const source = path.join(binDir, helper.product);
|
|
238
|
+
if (!fs.existsSync(source)) fail("NATIVE_HELPERS_INPUT_MISSING", `${helper.name}: the build did not produce ${source}`);
|
|
239
|
+
const found = archSet(run(`lipo ${helper.name}`, "lipo", ["-archs", source], { capture: true }).stdout);
|
|
240
|
+
if (found.join(" ") !== [...archs].sort().join(" ")) fail("NATIVE_HELPERS_ARCH_MISMATCH", `${helper.name}: ${source} holds [${found.join(" ")}], expected exactly [${[...archs].sort().join(" ")}]`, { found, expected: archs });
|
|
241
|
+
const dest = path.join(app, ...helper.embed.split("/"));
|
|
242
|
+
if (fs.existsSync(dest)) {
|
|
243
|
+
if (!replaceExisting) fail("NATIVE_HELPERS_EMBED_EXISTS", `${helper.name}: ${helper.embed} already exists in ${app}; something else wrote it (dev rebuilds pass replaceExisting)`);
|
|
244
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
245
|
+
}
|
|
246
|
+
let binary;
|
|
247
|
+
if (helper.kind === "executable") {
|
|
248
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
249
|
+
fs.copyFileSync(source, dest);
|
|
250
|
+
fs.chmodSync(dest, 0o755);
|
|
251
|
+
binary = dest;
|
|
252
|
+
} else {
|
|
253
|
+
const executable = helper.executable ?? helper.product;
|
|
254
|
+
binary = path.join(dest, "Contents", "MacOS", executable);
|
|
255
|
+
fs.mkdirSync(path.dirname(binary), { recursive: true });
|
|
256
|
+
fs.writeFileSync(path.join(dest, "Contents", "Info.plist"), helperInfoPlist({ helper, executable, version }));
|
|
257
|
+
fs.copyFileSync(source, binary);
|
|
258
|
+
fs.chmodSync(binary, 0o755);
|
|
259
|
+
for (const resource of helper.resources ?? []) {
|
|
260
|
+
const from = path.resolve(appRoot, resource.from);
|
|
261
|
+
if (!fs.existsSync(from)) fail("NATIVE_HELPERS_INPUT_MISSING", `${helper.name}: resource not found: ${from}`);
|
|
262
|
+
const to = path.join(dest, "Contents", "Resources", ...resource.to.split("/"));
|
|
263
|
+
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
264
|
+
fs.cpSync(from, to, { recursive: true, verbatimSymlinks: true });
|
|
265
|
+
}
|
|
266
|
+
if (helper.appIntents) embedAppIntents({ helper, run, fs, tmpdir, pkgDir, binDir, dest, archs, configuration });
|
|
267
|
+
if (helper.provisioningProfile) {
|
|
268
|
+
const profile = path.resolve(appRoot, helper.provisioningProfile);
|
|
269
|
+
if (!fs.existsSync(profile)) fail("NATIVE_HELPERS_INPUT_MISSING", `${helper.name}: provisioning profile not found: ${profile}`);
|
|
270
|
+
fs.copyFileSync(profile, path.join(dest, "Contents", "embedded.provisionprofile"));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
events.push(`embed:${helper.name}`);
|
|
274
|
+
results.push({ name: helper.name, embed: helper.embed, path: dest, source, archs: found, sha256: sha256(fs, binary) });
|
|
275
|
+
}
|
|
276
|
+
return { helpers: results, events };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** The appintentsmetadataprocessor step: SwiftPM leaves the const-value inputs under Intermediates.noindex (xcbuild layout). */
|
|
280
|
+
function embedAppIntents({ helper, run, fs, tmpdir, pkgDir, binDir, dest, archs, configuration }) {
|
|
281
|
+
const { moduleName, deploymentTarget } = helper.appIntents;
|
|
282
|
+
const cfg = configuration === "debug" ? "Debug" : "Release";
|
|
283
|
+
const arch = archs.includes("arm64") ? "arm64" : archs[0];
|
|
284
|
+
const intermediates = path.join(path.resolve(binDir, "../.."), "Intermediates.noindex");
|
|
285
|
+
const sourceList = findFile(fs, intermediates, (file) => file.endsWith(`/${moduleName}.SwiftFileList`) && file.includes(`/${cfg}/`) && file.includes(`/${arch}/`));
|
|
286
|
+
const constVals = sourceList
|
|
287
|
+
? fs.readdirSync(path.dirname(sourceList), { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".swiftconstvalues")).map((entry) => path.join(path.dirname(sourceList), entry.name)).sort()
|
|
288
|
+
: [];
|
|
289
|
+
if (!sourceList || !constVals.length) fail("NATIVE_HELPERS_APPINTENTS_FAILED", `${helper.name}: SwiftPM did not emit App Intents metadata inputs for ${moduleName} under ${intermediates}`);
|
|
290
|
+
const out = (what, cmd, args) => String(run(what, cmd, args, { capture: true }).stdout ?? "").trim();
|
|
291
|
+
const swiftc = out("xcrun swiftc", "xcrun", ["--find", "swiftc"]);
|
|
292
|
+
const sdkRoot = out("sdk path", "xcrun", ["--sdk", "macosx", "--show-sdk-path"]);
|
|
293
|
+
const xcodeVersion = /^Xcode (.+)$/m.exec(out("xcodebuild version", "xcodebuild", ["-version"]))?.[1];
|
|
294
|
+
if (!swiftc || !sdkRoot || !xcodeVersion) fail("NATIVE_HELPERS_APPINTENTS_FAILED", `${helper.name}: cannot determine swiftc, the macOS SDK and the Xcode version for App Intents metadata`);
|
|
295
|
+
const scratch = fs.mkdtempSync(path.join(tmpdir, "rightkit-appintents-"));
|
|
296
|
+
const list = path.join(scratch, "swift-const-values.list");
|
|
297
|
+
const output = path.join(dest, "Contents", "Resources");
|
|
298
|
+
try {
|
|
299
|
+
fs.writeFileSync(list, `${constVals.join("\n")}\n`);
|
|
300
|
+
run(`appintents metadata ${helper.name}`, path.join(path.dirname(swiftc), "appintentsmetadataprocessor"), [
|
|
301
|
+
"--output", output, "--toolchain-dir", path.dirname(path.dirname(path.dirname(swiftc))), "--module-name", moduleName, "--sdk-root", sdkRoot,
|
|
302
|
+
"--xcode-version", xcodeVersion, "--platform-family", "macOS", "--deployment-target", deploymentTarget, "--target-triple", `${arch}-apple-macosx${deploymentTarget}`,
|
|
303
|
+
"--source-file-list", sourceList, "--swift-const-vals-list", list, "--force"], { cwd: pkgDir });
|
|
304
|
+
} finally { fs.rmSync(scratch, { recursive: true, force: true }); }
|
|
305
|
+
if (!fs.existsSync(path.join(output, "Metadata.appintents", "extract.actionsdata"))) fail("NATIVE_HELPERS_APPINTENTS_FAILED", `${helper.name}: App Intents metadata generation produced no Metadata.appintents/extract.actionsdata`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const depth = (file) => file.split(/[\\/]/).length;
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Inside-out signing plan for embedded helpers: for every helper the code nested inside it first (a kind "app" helper's
|
|
312
|
+
* own frameworks and plug-ins; its main executable is sealed with its bundle), then the helper itself with its own
|
|
313
|
+
* entitlements and the hardened runtime; deepest path first overall. The app itself is NOT in the plan (it is signed last).
|
|
314
|
+
*/
|
|
315
|
+
export function planNativeHelperSigning({ app, appRoot, helpers: rawHelpers, discover = discoverNestedCode, readExecutable }) {
|
|
316
|
+
const helpers = validateNativeHelpers(rawHelpers);
|
|
317
|
+
const entries = [];
|
|
318
|
+
for (const helper of helpers) {
|
|
319
|
+
const file = path.join(app, ...helper.embed.split("/"));
|
|
320
|
+
if (helper.kind === "app") {
|
|
321
|
+
const executable = helper.executable ?? helper.product;
|
|
322
|
+
for (const nested of discover(file, readExecutable ? readExecutable(file) : executable)) entries.push({ file: nested, hardenedRuntime: true, options: [] });
|
|
323
|
+
}
|
|
324
|
+
entries.push({ file, hardenedRuntime: true, options: [], ...(helper.entitlements ? { entitlements: path.resolve(appRoot, helper.entitlements) } : {}) });
|
|
325
|
+
}
|
|
326
|
+
return entries.sort((a, b) => depth(b.file) - depth(a.file) || a.file.localeCompare(b.file));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Sign the helpers inside-out (see planNativeHelperSigning). deps: identity, signOne(codesignArgs), inspect(cmd, args) -> text. */
|
|
330
|
+
export function signNativeHelpers({ app, appRoot, helpers, identity, signOne, inspect, discover, evidence, hashFile }) {
|
|
331
|
+
if (typeof identity !== "string" || !identity.trim()) fail("NATIVE_HELPERS_SIGN_FAILED", "a signing identity is required");
|
|
332
|
+
const entries = planNativeHelperSigning({ app, appRoot, helpers, ...(discover ? { discover } : {}) });
|
|
333
|
+
return signMacosEntries(entries, { identity, signOne, inspect, ...(evidence ? { evidence } : {}), ...(hashFile ? { hashFile } : {}) });
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Whole sequence for a caller that owns signing itself: build helpers, embed, sign helpers inside-out, sign the app
|
|
338
|
+
* (`signApp()` supplied by the caller), verify (`codesign --verify --strict --deep`). Returns { build, signing, events }.
|
|
339
|
+
*/
|
|
340
|
+
export function buildEmbedSignNativeHelpers({ signApp, verify = true, ...options }) {
|
|
341
|
+
if (typeof signApp !== "function") fail("NATIVE_HELPERS_INVALID", "signApp is required: the app is signed after its helpers");
|
|
342
|
+
const built = buildAndEmbedNativeHelpers(options);
|
|
343
|
+
const events = [...built.events];
|
|
344
|
+
const signing = signNativeHelpers({ app: options.app, appRoot: options.appRoot, helpers: options.helpers, ...options.signing });
|
|
345
|
+
for (const record of signing) events.push(`sign:${path.relative(options.app, record.file).split(path.sep).join("/")}`);
|
|
346
|
+
signApp();
|
|
347
|
+
events.push("sign:app");
|
|
348
|
+
if (verify) {
|
|
349
|
+
const spawn = options.spawn ?? nodeSpawnSync;
|
|
350
|
+
const checked = spawn("codesign", ["--verify", "--strict", "--deep", options.app], { cwd: options.appRoot, env: options.env ?? process.env, encoding: "utf8", stdio: "pipe", windowsHide: true });
|
|
351
|
+
if (!checked || checked.error || checked.status !== 0) fail("NATIVE_HELPERS_VERIFY_FAILED", `codesign --verify --strict --deep failed for ${options.app}: ${tail(checked?.stderr)}`);
|
|
352
|
+
events.push("verify");
|
|
353
|
+
}
|
|
354
|
+
return { build: built, signing, events };
|
|
355
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// app-specific product domain logic and presentation only.
|
|
5
5
|
// Non-product code stays in an app only under an approved, expiring entry of .rightkit/ownership-exceptions.json that names the
|
|
6
6
|
// RightKit issue that moves it. These two rules run wherever the app check runs (`rightkit-gate check`, `right-release check`, the
|
|
7
|
-
//
|
|
7
|
+
// start of every checked right-release command and RightKit's own preflight):
|
|
8
8
|
//
|
|
9
9
|
// mandatory-foundation OW-05 an app whose profile requires a foundation owner the app does not declare as a dependency (missing,
|
|
10
10
|
// or dev-only), or that selects an unknown or unratified profile. An app is a repository whose root
|