@rightkit/release 0.2.174 → 0.4.0

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.
Files changed (82) hide show
  1. package/README.md +200 -2
  2. package/addon-command.mjs +3 -4
  3. package/android-play-upload.mjs +2 -1
  4. package/apple-profile-renew.mjs +11 -3
  5. package/artifact-storage.mjs +35 -2
  6. package/bin/darwin/rightkit-swap +0 -0
  7. package/bin/darwin/rightkit-swap.sign.json +1 -1
  8. package/build-release.mjs +138 -47
  9. package/cli/right-release.mjs +40 -5
  10. package/codesign-environment.mjs +2 -2
  11. package/command-spec.mjs +80 -0
  12. package/content-digest.mjs +209 -0
  13. package/deps-cargo.mjs +55 -0
  14. package/deps.mjs +7 -0
  15. package/development.mjs +17 -6
  16. package/direct-bootstrap.mjs +5 -6
  17. package/disk-command.mjs +1 -0
  18. package/disk-policy.json +6 -2
  19. package/disk-stable-root.mjs +13 -9
  20. package/dist-scan.mjs +102 -0
  21. package/dmg-build.mjs +53 -4
  22. package/github-updater-release.mjs +70 -7
  23. package/icon-pack.mjs +117 -0
  24. package/in-place-install.mjs +17 -14
  25. package/init-command.mjs +116 -0
  26. package/install-app-windows.mjs +24 -7
  27. package/install-app.mjs +56 -42
  28. package/ios-asc-credentials.mjs +163 -0
  29. package/ios-build-context.mjs +129 -0
  30. package/ios-build-upload.mjs +282 -0
  31. package/ios-credentials-command.mjs +70 -0
  32. package/ios-dev.mjs +248 -0
  33. package/ios-host.mjs +76 -0
  34. package/ios-lock.mjs +75 -0
  35. package/ios-package-recipe.mjs +707 -0
  36. package/ios-testflight.mjs +266 -0
  37. package/ios-upload.mjs +313 -0
  38. package/local-publish.mjs +390 -47
  39. package/macho-approval-hash.mjs +181 -0
  40. package/managed-cargo.mjs +57 -1
  41. package/media-sidecar.mjs +20 -4
  42. package/mirror-root-artifact.mjs +74 -22
  43. package/native-cargo-layout.mjs +47 -4
  44. package/npm-license-notices.mjs +159 -0
  45. package/ownership-command.mjs +2 -1
  46. package/ownership-dependency-allowlist.json +2 -1
  47. package/ownership-evidence.mjs +30 -18
  48. package/ownership-key-command.mjs +69 -2
  49. package/ownership-mandatory-set.mjs +66 -16
  50. package/ownership-repositories.json +1 -1
  51. package/ownership-scope.mjs +94 -0
  52. package/ownership.mjs +86 -11
  53. package/package-recipe-targets.mjs +56 -2
  54. package/package.json +1 -1
  55. package/pe-content.mjs +133 -0
  56. package/preflight.mjs +14 -7
  57. package/project-type.mjs +3 -1
  58. package/protected-credentials.mjs +28 -1
  59. package/publication-artifacts.mjs +30 -5
  60. package/publication-gate.mjs +83 -5
  61. package/publish-cargo.mjs +124 -18
  62. package/publish-update.mjs +15 -2
  63. package/qa-build-features.mjs +16 -13
  64. package/qa-contract.mjs +30 -2
  65. package/qa-control-scan.mjs +4 -0
  66. package/release-dependencies-command.mjs +45 -0
  67. package/release-dependencies.mjs +60 -0
  68. package/release-invocation.mjs +118 -8
  69. package/release-switch-registry.json +126 -27
  70. package/release-switch-scan.mjs +62 -24
  71. package/release.mjs +144 -21
  72. package/rightkit-ownership.json +46 -2
  73. package/rightkit-versions.json +380 -189
  74. package/runtime-budgets.mjs +134 -23
  75. package/sign-updater.mjs +56 -59
  76. package/source-gate.mjs +8 -1
  77. package/stage-sidecar.mjs +634 -69
  78. package/target-distribution.mjs +5 -0
  79. package/test-command.mjs +190 -0
  80. package/toolchain-swift.mjs +239 -0
  81. package/upload-release.mjs +7 -2
  82. package/web-build.mjs +67 -0
package/qa-contract.mjs CHANGED
@@ -106,6 +106,33 @@ export function qaManifests(appRoot, { readdir = readdirSync } = {}) {
106
106
  return found.sort();
107
107
  }
108
108
 
109
+ /**
110
+ * Native Apple UI-test targets under `appRoot`: an XcodeGen `project.yml` target of `type: bundle.ui-testing`,
111
+ * or an `.xcodeproj` whose `project.pbxproj` declares `com.apple.product-type.bundle.ui-testing`. An XCUITest
112
+ * target is the QA lane of a native iOS or macOS app, which has no package.json or Cargo.toml to pin RightKit QA in.
113
+ */
114
+ export function nativeAppleUiTestLanes(appRoot, { readdir = readdirSync, readFile = readFileSync } = {}) {
115
+ const lanes = [];
116
+ const read = (file) => { try { return readFile(file, "utf8"); } catch { return ""; } };
117
+ const visit = (dir, depth) => {
118
+ let entries;
119
+ try { entries = readdir(dir, { withFileTypes: true }); } catch { return; }
120
+ for (const entry of entries) {
121
+ const full = path.join(dir, entry.name);
122
+ const relative = path.relative(appRoot, full).split(path.sep).join("/");
123
+ if (entry.isDirectory()) {
124
+ if (entry.name.endsWith(".xcodeproj")) {
125
+ if (read(path.join(full, "project.pbxproj")).includes("com.apple.product-type.bundle.ui-testing")) lanes.push(`native XCUITest lane (${relative})`);
126
+ } else if (depth < MAX_DEPTH && !SKIP_DIRS.test(entry.name) && relative !== "rightkit" && relative !== "tools/rightkit") visit(full, depth + 1);
127
+ } else if (entry.name === "project.yml" && /^\s*type:\s*bundle\.ui-testing\s*$/m.test(read(full))) {
128
+ lanes.push(`native XCUITest lane (${relative})`);
129
+ }
130
+ }
131
+ };
132
+ visit(appRoot, 0);
133
+ return [...new Set(lanes)].sort();
134
+ }
135
+
109
136
  /** `[dependencies]`-style section header that holds dependencies (including target-specific ones). */
110
137
  const DEPENDENCY_HEADER = /^\[\s*(?:target\.[^\]]+\.)?(?:dev-|build-)?dependencies\s*\]/;
111
138
  const DEPENDENCY_TABLE_HEADER = /^\[\s*(?:target\.[^\]]+\.)?(?:dev-|build-)?dependencies\.([A-Za-z0-9_-]+)\s*\]/;
@@ -215,11 +242,12 @@ export function evaluateQaAdoption({ appRoot, repoRoot, app }, deps = {}) {
215
242
  }
216
243
  const label = app ?? "this app";
217
244
  const adopted = [...new Set(pins.filter((pin) => pin.name === NPM_QA || pin.name === CARGO_QA).map((pin) => pin.name))];
218
- const lane = exists(path.join(appRoot, "right-qa.config.mjs")) ? ["native wdio lane (right-qa.config.mjs)", ...adopted] : adopted;
245
+ const nativeApple = deps.nativeAppleLanes ?? nativeAppleUiTestLanes(appRoot, deps);
246
+ const lane = [...(exists(path.join(appRoot, "right-qa.config.mjs")) ? ["native wdio lane (right-qa.config.mjs)"] : []), ...nativeApple, ...adopted];
219
247
  const refusals = [];
220
248
  const warnings = [];
221
249
  if (lane.length === 0) {
222
- refusals.push(`RightKit QA is mandatory for every Orthic Labs app, and ${label} has no QA lane: no right-qa.config.mjs, no ${NPM_QA} in a package.json, no ${CARGO_QA} in a Cargo.toml${unreadable.length ? ` (unreadable: ${unreadable.join("; ")})` : ""}. Adopt it: add ${NPM_QA} to devDependencies (plus ${NPM_QA_CHROME} for a web UI) and/or ${CARGO_QA} to Cargo.toml [dev-dependencies], add the rightkit-qa floor run (smoke.launch, journey), see docs/ADOPTION.md, then run: node rightkit-drift.mjs --apply. Only a repository that is not an app is exempt (${QA_EXEMPTIONS_FILE}); an app cannot opt out`);
250
+ refusals.push(`RightKit QA is mandatory for every Orthic Labs app, and ${label} has no QA lane: no right-qa.config.mjs, no XCUITest target, no ${NPM_QA} in a package.json, no ${CARGO_QA} in a Cargo.toml${unreadable.length ? ` (unreadable: ${unreadable.join("; ")})` : ""}. Adopt it: add ${NPM_QA} to devDependencies (plus ${NPM_QA_CHROME} for a web UI) and/or ${CARGO_QA} to Cargo.toml [dev-dependencies], add the rightkit-qa floor run (smoke.launch, journey), see docs/ADOPTION.md, then run: node rightkit-drift.mjs --apply. Only a repository that is not an app is exempt (${QA_EXEMPTIONS_FILE}); an app cannot opt out`);
223
251
  }
224
252
  const lagging = pins
225
253
  .map((pin) => ({ ...pin, latest: pin.version && latestFor(versions, pin.ecosystem, pin.name) }))
@@ -379,6 +379,10 @@ export function assertNoQaControl(paths, label = "release", { requireBinary = fa
379
379
  // string that survives minification (an identifier that is also a string literal, a file/chunk name, a protocol or
380
380
  // a build-flag name). Source maps are scanned too: a map that names qaMock.ts proves the module was bundled.
381
381
  export const WEB_MOCK_MARKERS = Object.freeze([
382
+ { id: "platform-qa-selector", pattern: /__RIGHTKIT_QA_PLATFORM__/ },
383
+ { id: "platform-qa-environment", pattern: /\bRIGHTKIT_QA_PLATFORM\b/ },
384
+ { id: "platform-qa-entry", pattern: /(?:bootstrapQaPlatform|resolveQaPlatform|configureQaPlatform|@rightkit\/platform-ui\/qa)/ },
385
+
382
386
  { id: "browser-qa-fake", pattern: /browser-qa-fake/ },
383
387
  { id: "browser-mock-api", pattern: /browser-mock-api/ },
384
388
  { id: "browser-qa-mock-scheme", pattern: /browser-qa-mock:/ },
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env node
2
+ import path from "node:path";
3
+ import { fileURLToPath, pathToFileURL } from "node:url";
4
+ import { assertReleaseDependencies } from "./release-dependencies.mjs";
5
+ import { nodeHostTriple } from "./stage-sidecar-command.mjs";
6
+ import { expandPackageRecipeTarget } from "./package-recipe-targets.mjs";
7
+
8
+ /** Standalone policy check: host target by default, all declared release targets on request. */
9
+ export async function main(argv, { cwd = process.cwd(), platform = process.platform, arch = process.arch, run } = {}) {
10
+ let configPath = "right-release.config.mjs", selected, allTargets = false;
11
+ for (let i = 0; i < argv.length; i += 1) {
12
+ const arg = argv[i];
13
+ if (["--config", "--platform"].includes(arg)) {
14
+ const value = argv[++i];
15
+ if (!value || value.startsWith("--")) throw new Error(`${arg} needs a value`);
16
+ if (arg === "--config") configPath = value; else selected = value;
17
+ } else if (arg === "--all-targets") allTargets = true;
18
+ else throw new Error(`unknown check-release-dependencies argument: ${arg}`);
19
+ }
20
+ if (allTargets && selected) throw new Error("use --platform or --all-targets");
21
+ const file = path.resolve(cwd, configPath);
22
+ const root = path.dirname(file);
23
+ const config = (await import(pathToFileURL(file).href)).default;
24
+ if (config?.releaseDependencies === undefined) throw new Error("check-release-dependencies requires releaseDependencies in config");
25
+ const host = { darwin: "mac", win32: "win", linux: "linux" }[platform];
26
+ const platforms = allTargets ? Object.keys(config.targets ?? {}) : [selected ?? host];
27
+ if (!platforms.length) throw new Error("no release targets declared");
28
+ const results = [];
29
+ for (const name of platforms) {
30
+ let target = config.targets?.[name];
31
+ if (!name || !target) throw new Error(`no release target for ${name ?? platform}; use --platform or --all-targets`);
32
+ target = expandPackageRecipeTarget(target, { platform: name, appRoot: root, config });
33
+ if (name !== host && !(target.cargoTarget ?? target.targetTriple ?? target.rustTarget)) throw new Error(`${name} must declare cargoTarget, targetTriple or rustTarget`);
34
+ if (!allTargets && !selected) target = { ...target, cargoTarget: nodeHostTriple(platform, arch) };
35
+ results.push(assertReleaseDependencies({ root, config, target, run }));
36
+ }
37
+ return results;
38
+ }
39
+
40
+ if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
41
+ main(process.argv.slice(2)).then((results) => console.log(JSON.stringify(results, null, 2))).catch((error) => {
42
+ console.error(`right-release check-release-dependencies: ${error.message}`);
43
+ process.exitCode = 1;
44
+ });
45
+ }
@@ -0,0 +1,60 @@
1
+ // App-specific release dependency policy. Development builds never call this gate.
2
+ import { existsSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { resolveCargoTree } from "./release-switch-scan.mjs";
5
+ import { expandRecipeBuildCommands } from "./recipe-build.mjs";
6
+ import { nodeHostTriple } from "./stage-sidecar-command.mjs";
7
+ import { resolveNativeFinalizerConfig } from "./native-release-finalization.mjs";
8
+
9
+ function crateNames(names) {
10
+ if (!Array.isArray(names) || names.some((name) => typeof name !== "string" || !/^[A-Za-z0-9_][A-Za-z0-9_-]*$/.test(name))) {
11
+ throw new TypeError("releaseDependencies.forbiddenCrates must be an array of exact Cargo package names");
12
+ }
13
+ return [...new Set(names)];
14
+ }
15
+
16
+ /** Exact package matches from `cargo tree -e normal --prefix none`, deduplicated across versions. */
17
+ export function forbiddenCrateFindings(treeText, forbiddenCrates) {
18
+ const denied = new Set(crateNames(forbiddenCrates));
19
+ const found = new Set();
20
+ for (const line of String(treeText).split(/\r?\n/)) {
21
+ const match = line.match(/^([A-Za-z0-9_][A-Za-z0-9_-]*) v\d[^\s]*(?:\s|$)/);
22
+ if (match && denied.has(match[1])) found.add(match[1]);
23
+ }
24
+ return [...found].sort();
25
+ }
26
+
27
+ /**
28
+ * Enforce config.releaseDependencies for one release target before packaging.
29
+ * `commands` may supply already-expanded release hooks; `run` injects the Cargo runner for tests.
30
+ * Returns { skipped } without Cargo when no policy is declared, else { manifest, target, trees, forbiddenCrates }.
31
+ * Cargo errors, empty output, invalid policy & forbidden packages throw; dry runs obey this gate too.
32
+ */
33
+ export function assertReleaseDependencies({ root, config = {}, target = {}, commands, run } = {}) {
34
+ const policy = config.releaseDependencies;
35
+ if (policy === undefined) return { skipped: "no releaseDependencies policy" };
36
+ if (!policy || typeof policy !== "object" || Array.isArray(policy)) throw new TypeError("releaseDependencies must be an object");
37
+ const forbiddenCrates = crateNames(policy.forbiddenCrates);
38
+ if (policy.features !== undefined && (!Array.isArray(policy.features) || policy.features.some((feature) => typeof feature !== "string" || !feature.trim() || feature.startsWith("-")))) {
39
+ throw new TypeError("releaseDependencies.features must be an array of Cargo feature names");
40
+ }
41
+ if (policy.noDefaultFeatures !== undefined && typeof policy.noDefaultFeatures !== "boolean") throw new TypeError("releaseDependencies.noDefaultFeatures must be boolean");
42
+ const manifest = policy.manifest ?? target.cargoManifest ?? config.releaseSwitches?.manifest ?? config.nativeAssembly?.cargoManifest ?? config.nativeAssembly?.manifest;
43
+ if (manifest !== undefined && (typeof manifest !== "string" || !manifest || !existsSync(path.resolve(root, manifest)))) {
44
+ throw new Error(`release dependencies: declared Cargo manifest does not exist: ${manifest}`);
45
+ }
46
+ const triple = target.cargoTarget ?? target.targetTriple ?? target.rustTarget ?? nodeHostTriple();
47
+ const commandList = (value) => value == null ? [] : Array.isArray(value) ? value : [value];
48
+ const hooks = commands ?? [target.prePackage, target.package, config.nativeAssembly?.packageHook,
49
+ resolveNativeFinalizerConfig(config.nativeAssembly), ...commandList(target.postSign), target.gradle].filter(Boolean);
50
+ const closure = resolveCargoTree({
51
+ root, manifest: manifest === undefined ? undefined : path.relative(root, path.resolve(root, manifest)), features: [...(config.releaseSwitches?.features ?? []), ...(policy.features ?? [])],
52
+ noDefaultFeatures: policy.noDefaultFeatures ?? false,
53
+ triple, commandTexts: hooks.flatMap((command) => expandRecipeBuildCommands(command, { root })),
54
+ edges: "normal", prefix: "none", label: "release dependencies", run,
55
+ });
56
+ if (!/^[A-Za-z0-9_][A-Za-z0-9_-]* v\d[^\s]*(?:\s|$)/m.test(closure.treeText)) throw new Error("release dependencies: cargo tree returned no package records");
57
+ const found = forbiddenCrateFindings(closure.treeText, forbiddenCrates);
58
+ if (found.length) throw new Error(found.map((crate) => `FAIL: ${crate} is in the release dependency tree (${triple})`).join("\n"));
59
+ return { manifest: closure.manifest, target: triple, trees: closure.trees.length, forbiddenCrates };
60
+ }
@@ -2,7 +2,7 @@ import { spawnSync } from "./background-process.mjs";
2
2
  import { resolveProjectType } from "./project-type.mjs";
3
3
  import { existsSync, readFileSync, realpathSync } from "node:fs";
4
4
  import path from "node:path";
5
- import { resolveNativeCargoLayout } from "./native-cargo-layout.mjs";
5
+ import { declaredCargoManifest, hasDefaultTauriManifest, resolveNativeCargoLayout } from "./native-cargo-layout.mjs";
6
6
 
7
7
  function git(cwd, args) {
8
8
  const result = spawnSync("git", args, { cwd, encoding: "utf8", windowsHide: true });
@@ -129,20 +129,128 @@ export function resolveConfiguredBuildInputs(config, target, label = "release co
129
129
  return configured;
130
130
  }
131
131
 
132
+ /** The validated or raw iOS package spec of a target (`targets.ios.iosPackage`, or the recipe the build expands it to). */
133
+ export function iosPackageSpecOf(target) {
134
+ if (target?.recipe?.kind === "iosPackage") return target.recipe.spec ?? {};
135
+ if (target?.iosPackage && typeof target.iosPackage === "object") return target.iosPackage;
136
+ return null;
137
+ }
138
+
139
+ /**
140
+ * Native iOS mode: an iOS target with iosPackage and no src-tauri/Cargo.toml (a declared nativeAssembly.cargoManifest does not
141
+ * leave it). The app is an xcodegen project built by xcodebuild, not a Tauri host: no broker target, no target bridge, no
142
+ * Tauri layout. A declared manifest only pins its Cargo.toml and Cargo.lock as inputs; iosPackage.prepare runs the cargo build.
143
+ * An iOS app that has src-tauri/Cargo.toml (Pulse) keeps the Cargo-backed behaviour exactly.
144
+ */
145
+ export function isNativeIosTarget({ appRoot, target, exists } = {}) {
146
+ return iosPackageSpecOf(target) !== null && !hasDefaultTauriManifest({ appRoot, exists });
147
+ }
148
+
149
+ /**
150
+ * prepare commands exist to build the app's Rust static library. Without a declared nativeAssembly.cargoManifest the release
151
+ * would hash and seal none of that Rust source graph, so a native iOS target may not carry them.
152
+ */
153
+ export function assertNativeIosPrepareHasManifest(iosSpec, config, appRoot) {
154
+ if (declaredCargoManifest(config)) {
155
+ const problems = nativeIosPrepareProblems({ appRoot, config, prepare: iosSpec?.prepare ?? [] });
156
+ if (problems.length) throw new Error(`iosPackage.prepare must be only bare cargo commands when a Rust manifest is declared; express the Rust build as cargo commands in prepare (no node, scripts, pnpm, python, rustup run, +toolchain or other manifest-path), so the features it enables can be checked and the release pins exactly the manifest it builds:\n- ${problems.join("\n- ")}`);
157
+ return;
158
+ }
159
+ if (Array.isArray(iosSpec?.prepare) && iosSpec.prepare.length > 0) {
160
+ throw new Error("targets.ios.iosPackage.prepare builds a Rust library but no Cargo manifest is declared; declare nativeAssembly.cargoManifest (and keep its Cargo.lock beside it, or name it with nativeAssembly.cargoLockPath) so the Rust manifest and lock are pinned as required release inputs");
161
+ }
162
+ }
163
+
164
+ const FEATURE_FLAGS = new Set(["--features", "-F", "--feature"]);
165
+ // The only cargo subcommands a native iOS prepare may run: each takes --features and builds exactly one manifest. xtask, aliases
166
+ // (which .cargo/config.toml can point anywhere) and cargo-<custom> subcommands are opaque to the feature scan.
167
+ const PREPARE_SUBCOMMANDS = new Set(["build", "rustc", "check"]);
168
+
169
+ /**
170
+ * With a declared Rust manifest, every prepare command is a bare `cargo`: nothing else can be scanned for the features it enables
171
+ * or be trusted to build only the manifest the release pins. Returns the reasons a command is refused (empty when all are fine).
172
+ */
173
+ export function nativeIosPrepareProblems({ appRoot, config, prepare = [] }) {
174
+ const declared = declaredCargoManifest(config);
175
+ if (!declared) return [];
176
+ const manifest = path.resolve(appRoot, declared);
177
+ const problems = [];
178
+ prepare.forEach((command, index) => {
179
+ const where = `targets.ios.iosPackage.prepare[${index}] (${command?.cmd})`;
180
+ if (command?.cmd !== "cargo") { problems.push(`${where} is not a bare cargo command`); return; }
181
+ const args = (command.args ?? []).map(String);
182
+ if (args[0]?.startsWith("+")) problems.push(`${where} selects a toolchain with ${args[0]}`);
183
+ else if (!PREPARE_SUBCOMMANDS.has(args[0])) problems.push(`${where} runs cargo ${args[0] ?? "(no subcommand)"}; only cargo build, rustc and check are allowed (no xtask, alias or cargo-<custom> subcommand)`);
184
+ const cwd = path.resolve(appRoot, command.cwd ?? ".");
185
+ let manifestPathSeen = false;
186
+ args.forEach((arg, position) => {
187
+ if (arg === "--config" || arg.startsWith("--config=")) problems.push(`${where} passes ${arg.split("=")[0]}, which can change what cargo builds`);
188
+ if (arg === "-Z" || /^-Z.+/.test(arg)) problems.push(`${where} passes the unstable flag ${arg.split("=")[0]}`);
189
+ if (arg === "--target-dir" || arg.startsWith("--target-dir=")) problems.push(`${where} passes --target-dir; build output goes where the release expects it`);
190
+ const pathValue = arg === "--manifest-path" ? args[position + 1] : arg.startsWith("--manifest-path=") ? arg.slice("--manifest-path=".length) : undefined;
191
+ if (pathValue !== undefined) manifestPathSeen = true;
192
+ if (pathValue !== undefined && path.relative(path.resolve(cwd, pathValue), manifest) !== "") problems.push(`${where} builds ${pathValue}, not the declared manifest ${declared}`);
193
+ const featureValue = FEATURE_FLAGS.has(arg) ? args[position + 1] : arg.startsWith("--features=") ? arg.slice("--features=".length) : /^-F.+/.test(arg) ? arg.slice(2) : undefined;
194
+ if (featureValue !== undefined && /[$%`]/.test(featureValue)) problems.push(`${where} selects features from a variable (${arg}); the Cargo features it enables cannot be determined`);
195
+ });
196
+ // No cwd fallback: cargo would otherwise build whatever Cargo.toml the working directory holds.
197
+ if (!manifestPathSeen) problems.push(`${where} has no --manifest-path; pass --manifest-path ${declared} so it builds exactly the declared manifest`);
198
+ // The lock is hashed before prepare runs; without --locked cargo may rewrite it and build different dependencies.
199
+ if (!args.some((arg) => arg === "--locked" || arg === "--frozen")) problems.push(`${where} has no --locked; pass --locked so cargo builds exactly the pinned Cargo.lock`);
200
+ });
201
+ return problems;
202
+ }
203
+
204
+ /** The repo-relative-to-app Package.resolved an iosPackage with trusted Swift macros/plugins pins, or null when it trusts none. */
205
+ export function iosTrustPackageResolved(iosSpec) {
206
+ const listed = (value) => Array.isArray(value) && value.length > 0;
207
+ if (!listed(iosSpec?.trustedSwiftMacros) && !listed(iosSpec?.trustedSwiftPlugins)) return null;
208
+ if (typeof iosSpec.packageResolved === "string" && iosSpec.packageResolved) return iosSpec.packageResolved;
209
+ if (typeof iosSpec.project !== "string" || !iosSpec.project) return null;
210
+ return path.posix.join(iosSpec.project.replaceAll("\\", "/"), "project.xcworkspace", "xcshareddata", "swiftpm", "Package.resolved");
211
+ }
212
+
213
+ const outside = (root, file) => { const relative = path.relative(root, file); return relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative); };
214
+
132
215
  export function resolveReleaseBuildInputs({ repoRoot, appRoot, configPath, config, target, env = process.env }) {
133
216
  const swift = resolveProjectType({ appRoot, config, target }) === "swiftpm";
134
- const nativeLayout = resolveNativeCargoLayout({
135
- appRoot,
136
- config,
137
- requireManifest: !swift,
138
- requireLock: !swift,
139
- });
217
+ const iosSpec = iosPackageSpecOf(target);
218
+ const nativeIos = !swift && isNativeIosTarget({ appRoot, target });
219
+ if (nativeIos) assertNativeIosPrepareHasManifest(iosSpec, config, appRoot);
220
+ // Native iOS: a declared manifest is resolved only to pin its Cargo.toml and lock (refused when there is no lock, or when
221
+ // either lies outside the repository). The returned layout is null, so no consumer links a broker target or bridges a cargo target.
222
+ const declaredManifest = nativeIos ? declaredCargoManifest(config) : undefined;
223
+ if (declaredManifest && outside(repoRoot, path.resolve(appRoot, declaredManifest))) throw new Error(`declared Cargo manifest ${declaredManifest} is outside the repository ${repoRoot}; a release pins only sources it can seal`);
224
+ const pinnedLayout = nativeIos
225
+ ? (declaredManifest ? resolveNativeCargoLayout({ appRoot, config, requireManifest: true, requireLock: true, repoRoot }) : null)
226
+ : resolveNativeCargoLayout({
227
+ appRoot,
228
+ config,
229
+ requireManifest: !swift,
230
+ requireLock: !swift,
231
+ });
232
+ if (nativeIos && pinnedLayout && outside(repoRoot, pinnedLayout.lockPath)) throw new Error(`Cargo.lock ${pinnedLayout.lockPath} is outside the repository ${repoRoot}; a release pins only sources it can seal`);
233
+ const nativeLayout = nativeIos ? null : pinnedLayout;
234
+ const optionalIfPresent = (names) => names.map((f) => path.join(appRoot, f)).filter((f) => existsSync(f));
235
+ // The xcodegen spec is an input whenever iosPackage names one (a declared-but-missing spec is refused below).
236
+ // Only native iOS mode: an iOS app with src-tauri keeps exactly its pre-existing input set.
237
+ const iosProjectSpec = nativeIos && typeof iosSpec?.projectSpec === "string" && iosSpec.projectSpec ? [path.resolve(appRoot, iosSpec.projectSpec)] : [];
238
+ // Reviewed Swift macro/plugin trust (iosPackage.trustedSwiftMacros / trustedSwiftPlugins): the Package.resolved holding the pins is a
239
+ // required, hashed, git-tracked release input on both iOS paths (native and src-tauri). Without a list nothing is added.
240
+ const packageResolvedInput = iosTrustPackageResolved(iosSpec);
241
+ if (packageResolvedInput && outside(repoRoot, path.resolve(appRoot, packageResolvedInput))) throw new Error(`Package.resolved ${packageResolvedInput} is outside the repository ${repoRoot}; a release pins only sources it can seal`);
242
+ const iosPackageResolved = packageResolvedInput ? [path.resolve(appRoot, packageResolvedInput)] : [];
140
243
  const requiredInputs = [
141
244
  configPath,
142
245
  ...(swift
143
246
  // SwiftPM: Package.swift is required; Package.resolved and the Node manifests only when they exist.
144
- ? [path.join(appRoot, "Package.swift"), ...["Package.resolved", "package.json", "pnpm-lock.yaml"].map((f) => path.join(appRoot, f)).filter((f) => existsSync(f))]
247
+ ? [path.join(appRoot, "Package.swift"), ...optionalIfPresent(["Package.resolved", "package.json", "pnpm-lock.yaml"])]
248
+ : nativeIos
249
+ // Native iOS: package.json and pnpm-lock.yaml only when the app has them, plus the declared Cargo manifest and its lock.
250
+ ? [...optionalIfPresent(["package.json", "pnpm-lock.yaml"]), ...(pinnedLayout ? [pinnedLayout.manifestPath, pinnedLayout.lockPath] : [])]
145
251
  : [path.join(appRoot, "package.json"), path.join(appRoot, "pnpm-lock.yaml"), nativeLayout.manifestPath, nativeLayout.lockPath]),
252
+ ...iosProjectSpec,
253
+ ...iosPackageResolved,
146
254
  ...(target?.preflight?.files ?? []).map((file) => path.resolve(appRoot, expandEnv(file, env))),
147
255
  ].filter((file, index, files) => files.indexOf(file) === index);
148
256
  for (const file of requiredInputs) {
@@ -162,6 +270,8 @@ export function resolveReleaseBuildInputs({ repoRoot, appRoot, configPath, confi
162
270
  return {
163
271
  requiredInputs,
164
272
  nativeLayout,
273
+ // The Cargo.lock a native iOS app pins (its layout is null by design); preflight checks it like any other lock.
274
+ cargoLockPaths: nativeIos && pinnedLayout ? [pinnedLayout.lockPath] : [],
165
275
  buildInputs: {
166
276
  include: configured.include.map(qualify),
167
277
  exclude: (configured.exclude ?? []).map(qualify),
@@ -1,33 +1,132 @@
1
1
  {
2
2
  "schema": 1,
3
3
  "owner": "rightkit-daemon::mode (D4, ratified)",
4
- "forbiddenFeatures": ["test-mode", "qa-native", "dev-native", "qa-control", "dev-inspect"],
4
+ "forbiddenFeatures": [
5
+ "test-mode",
6
+ "qa-native",
7
+ "dev-native",
8
+ "qa-control",
9
+ "dev-inspect",
10
+ "testing",
11
+ "test-seams",
12
+ "isolated-test-keychain",
13
+ "demo",
14
+ "dev-overrides",
15
+ "qa-probes",
16
+ "dev-worker-override",
17
+ "qa-shot",
18
+ "qa-tools",
19
+ "dev-control",
20
+ "bench-native",
21
+ "rightkit-qa/control"
22
+ ],
5
23
  "testModeMarker": "RIGHTKIT-TEST-MODE-COMPILED",
6
24
  "denyNames": [
7
- { "name": "RIGHTKIT_QA_NATIVE", "class": "S1" },
8
- { "name": "RIGHTKIT_QA_HIDDEN", "class": "S1" },
9
- { "name": "RIGHTKIT_QA_DATA_DIR", "class": "S4" },
10
- { "name": "RIGHTKIT_QA_BACKGROUND", "class": "S1" },
11
- { "name": "RIGHTKIT_UNDER_TEST", "class": "S1" },
12
- { "name": "RIGHTKIT_SUITE_ROOT", "class": "S4" },
13
- { "name": "RIGHT_SUITE_DIR", "class": "S4" },
14
- { "name": "ORTHIC_SHARED_DIR", "class": "S4" },
15
- { "name": "RIGHTKIT_CONTROL_SERVICE", "class": "S3" },
16
- { "name": "HEARDRIGHT_ENGINE_TEST_MODE", "class": "S1" },
17
- { "name": "HR_APP_DATA_DIR", "class": "S4" },
18
- { "name": "MAILRIGHT_DATA_DIR", "class": "S4" },
19
- { "name": "MAILRIGHT_EMBED_MODEL_DIR", "class": "S3" },
20
- { "name": "MAILRIGHT_ONNXRUNTIME_DYLIB", "class": "S3" },
21
- { "name": "CUTRIGHT_DATA_DIR", "class": "S4" },
22
- { "name": "GENRIGHT_QA", "class": "S1" },
23
- { "name": "GENRIGHT_APP_DATA", "class": "S4" },
24
- { "name": "GENRIGHT_SD_SERVER", "class": "S3" },
25
- { "name": "CODERIGHT_HOME", "class": "S4" },
26
- { "name": "POSTRIGHT_ROLE=daemon-dev", "class": "S1" },
27
- { "name": "RIGHTAPP_ENGINE_BIN", "class": "S3" },
28
- { "name": "LIGHTCRAFT_SAM3_DIR", "class": "S3" },
29
- { "name": "LIGHTCRAFT_DEVICE_ROOTS", "class": "S3" },
30
- { "name": "--qa", "class": "S1" },
31
- { "name": "--demo", "class": "S1" }
32
- ]
25
+ {
26
+ "name": "RIGHTKIT_QA_NATIVE",
27
+ "class": "S1"
28
+ },
29
+ {
30
+ "name": "RIGHTKIT_QA_HIDDEN",
31
+ "class": "S1"
32
+ },
33
+ {
34
+ "name": "RIGHTKIT_QA_DATA_DIR",
35
+ "class": "S4"
36
+ },
37
+ {
38
+ "name": "RIGHTKIT_QA_BACKGROUND",
39
+ "class": "S1"
40
+ },
41
+ {
42
+ "name": "RIGHTKIT_UNDER_TEST",
43
+ "class": "S1"
44
+ },
45
+ {
46
+ "name": "RIGHTKIT_SUITE_ROOT",
47
+ "class": "S4"
48
+ },
49
+ {
50
+ "name": "RIGHT_SUITE_DIR",
51
+ "class": "S4"
52
+ },
53
+ {
54
+ "name": "ORTHIC_SHARED_DIR",
55
+ "class": "S4"
56
+ },
57
+ {
58
+ "name": "RIGHTKIT_CONTROL_SERVICE",
59
+ "class": "S3"
60
+ },
61
+ {
62
+ "name": "HEARDRIGHT_ENGINE_TEST_MODE",
63
+ "class": "S1"
64
+ },
65
+ {
66
+ "name": "HR_APP_DATA_DIR",
67
+ "class": "S4"
68
+ },
69
+ {
70
+ "name": "MAILRIGHT_DATA_DIR",
71
+ "class": "S4"
72
+ },
73
+ {
74
+ "name": "MAILRIGHT_EMBED_MODEL_DIR",
75
+ "class": "S3"
76
+ },
77
+ {
78
+ "name": "MAILRIGHT_ONNXRUNTIME_DYLIB",
79
+ "class": "S3"
80
+ },
81
+ {
82
+ "name": "CUTRIGHT_DATA_DIR",
83
+ "class": "S4"
84
+ },
85
+ {
86
+ "name": "GENRIGHT_QA",
87
+ "class": "S1"
88
+ },
89
+ {
90
+ "name": "GENRIGHT_APP_DATA",
91
+ "class": "S4"
92
+ },
93
+ {
94
+ "name": "GENRIGHT_SD_SERVER",
95
+ "class": "S3"
96
+ },
97
+ {
98
+ "name": "CODERIGHT_HOME",
99
+ "class": "S4"
100
+ },
101
+ {
102
+ "name": "POSTRIGHT_ROLE=daemon-dev",
103
+ "class": "S1"
104
+ },
105
+ {
106
+ "name": "RIGHTAPP_ENGINE_BIN",
107
+ "class": "S3"
108
+ },
109
+ {
110
+ "name": "LIGHTCRAFT_SAM3_DIR",
111
+ "class": "S3"
112
+ },
113
+ {
114
+ "name": "LIGHTCRAFT_DEVICE_ROOTS",
115
+ "class": "S3"
116
+ },
117
+ {
118
+ "name": "--qa",
119
+ "class": "S1"
120
+ },
121
+ {
122
+ "name": "--demo",
123
+ "class": "S1"
124
+ }
125
+ ],
126
+ "conditionalForbiddenFeatures": {
127
+ "content-only": [
128
+ "devtools",
129
+ "content-devtools"
130
+ ]
131
+ }
33
132
  }