@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.
- package/README.md +200 -2
- package/addon-command.mjs +3 -4
- package/android-play-upload.mjs +2 -1
- package/apple-profile-renew.mjs +11 -3
- package/artifact-storage.mjs +35 -2
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/darwin/rightkit-swap.sign.json +1 -1
- package/build-release.mjs +138 -47
- package/cli/right-release.mjs +40 -5
- package/codesign-environment.mjs +2 -2
- package/command-spec.mjs +80 -0
- package/content-digest.mjs +209 -0
- package/deps-cargo.mjs +55 -0
- package/deps.mjs +7 -0
- package/development.mjs +17 -6
- package/direct-bootstrap.mjs +5 -6
- package/disk-command.mjs +1 -0
- package/disk-policy.json +6 -2
- package/disk-stable-root.mjs +13 -9
- package/dist-scan.mjs +102 -0
- package/dmg-build.mjs +53 -4
- package/github-updater-release.mjs +70 -7
- package/icon-pack.mjs +117 -0
- package/in-place-install.mjs +17 -14
- package/init-command.mjs +116 -0
- package/install-app-windows.mjs +24 -7
- package/install-app.mjs +56 -42
- package/ios-asc-credentials.mjs +163 -0
- package/ios-build-context.mjs +129 -0
- package/ios-build-upload.mjs +282 -0
- package/ios-credentials-command.mjs +70 -0
- package/ios-dev.mjs +248 -0
- package/ios-host.mjs +76 -0
- package/ios-lock.mjs +75 -0
- package/ios-package-recipe.mjs +707 -0
- package/ios-testflight.mjs +266 -0
- package/ios-upload.mjs +313 -0
- package/local-publish.mjs +390 -47
- package/macho-approval-hash.mjs +181 -0
- package/managed-cargo.mjs +57 -1
- package/media-sidecar.mjs +20 -4
- package/mirror-root-artifact.mjs +74 -22
- package/native-cargo-layout.mjs +47 -4
- package/npm-license-notices.mjs +159 -0
- package/ownership-command.mjs +2 -1
- package/ownership-dependency-allowlist.json +2 -1
- package/ownership-evidence.mjs +30 -18
- package/ownership-key-command.mjs +69 -2
- package/ownership-mandatory-set.mjs +66 -16
- package/ownership-repositories.json +1 -1
- package/ownership-scope.mjs +94 -0
- package/ownership.mjs +86 -11
- package/package-recipe-targets.mjs +56 -2
- package/package.json +1 -1
- package/pe-content.mjs +133 -0
- package/preflight.mjs +14 -7
- package/project-type.mjs +3 -1
- package/protected-credentials.mjs +28 -1
- package/publication-artifacts.mjs +30 -5
- package/publication-gate.mjs +83 -5
- package/publish-cargo.mjs +124 -18
- package/publish-update.mjs +15 -2
- package/qa-build-features.mjs +16 -13
- package/qa-contract.mjs +30 -2
- package/qa-control-scan.mjs +4 -0
- package/release-dependencies-command.mjs +45 -0
- package/release-dependencies.mjs +60 -0
- package/release-invocation.mjs +118 -8
- package/release-switch-registry.json +126 -27
- package/release-switch-scan.mjs +62 -24
- package/release.mjs +144 -21
- package/rightkit-ownership.json +46 -2
- package/rightkit-versions.json +380 -189
- package/runtime-budgets.mjs +134 -23
- package/sign-updater.mjs +56 -59
- package/source-gate.mjs +8 -1
- package/stage-sidecar.mjs +634 -69
- package/target-distribution.mjs +5 -0
- package/test-command.mjs +190 -0
- package/toolchain-swift.mjs +239 -0
- package/upload-release.mjs +7 -2
- package/web-build.mjs +67 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Ownership scan scope for a target that builds from only part of its repository (iOS).
|
|
2
|
+
// A scope is a sorted, de-duplicated list of repository-relative directory roots. Only committed files under those roots
|
|
3
|
+
// are inventoried; the exceptions file under `.rightkit` is always read. Sealed ownership evidence records the scope and
|
|
4
|
+
// upload-time verification re-derives it from the config, so the scope cannot be narrowed after the build.
|
|
5
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
|
|
8
|
+
export const OWNERSHIP_SCOPE_ALWAYS = ".rightkit";
|
|
9
|
+
|
|
10
|
+
/** Normalize and validate repository-relative scope roots; throws a clear error for absolute, escaping, or missing roots. */
|
|
11
|
+
export function normalizeScopeRoots(roots, { repoRoot, label = "ownershipScope" } = {}) {
|
|
12
|
+
if (!Array.isArray(roots)) throw new Error(`${label} must be an array of repository-relative directories`);
|
|
13
|
+
const out = new Set();
|
|
14
|
+
for (const raw of roots) {
|
|
15
|
+
if (typeof raw !== "string" || !raw.trim()) throw new Error(`${label} entries must be non-empty strings`);
|
|
16
|
+
const slashed = raw.replaceAll("\\", "/");
|
|
17
|
+
if (path.posix.isAbsolute(slashed) || path.win32.isAbsolute(raw) || /^[A-Za-z]:/.test(slashed)) throw new Error(`${label} entry ${raw} must be repository-relative, not absolute`);
|
|
18
|
+
const normal = path.posix.normalize(slashed).replace(/\/+$/, "");
|
|
19
|
+
if (normal === ".." || normal.startsWith("../")) throw new Error(`${label} entry ${raw} escapes the repository`);
|
|
20
|
+
if (normal === "." || normal === "") throw new Error(`${label} entry ${raw} is the whole repository; omit the scope instead`);
|
|
21
|
+
if (repoRoot) {
|
|
22
|
+
const absolute = path.join(repoRoot, ...normal.split("/"));
|
|
23
|
+
let directory = false;
|
|
24
|
+
try { directory = existsSync(absolute) && statSync(absolute).isDirectory(); } catch { directory = false; }
|
|
25
|
+
if (!directory) throw new Error(`${label} entry ${raw} is not an existing directory in the repository`);
|
|
26
|
+
}
|
|
27
|
+
out.add(normal);
|
|
28
|
+
}
|
|
29
|
+
return [...out];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Local SwiftPM package directories named by `path:` inside the `packages:` section of an XcodeGen project.yml. */
|
|
33
|
+
export function projectYmlLocalPackagePaths(text) {
|
|
34
|
+
const lines = String(text).split(/\r?\n/);
|
|
35
|
+
const paths = [];
|
|
36
|
+
let inPackages = false;
|
|
37
|
+
for (const line of lines) {
|
|
38
|
+
if (/^\s*(#.*)?$/.test(line)) continue;
|
|
39
|
+
if (/^\S/.test(line)) { inPackages = /^packages:\s*(#.*)?$/.test(line); continue; }
|
|
40
|
+
if (!inPackages) continue;
|
|
41
|
+
const match = /(?:^|[\s{,])path:\s*(?:"([^"]*)"|'([^']*)'|([^\s,}#]+))/.exec(line);
|
|
42
|
+
if (match) paths.push(match[1] ?? match[2] ?? match[3]);
|
|
43
|
+
}
|
|
44
|
+
return paths;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Scan scope of an iOS target: the Xcode project directory, each local SwiftPM package its project.yml references,
|
|
49
|
+
* `.rightkit` (exceptions), the native Rust assembly (`nativeAssembly.workspaceRoot` and the directory of its cargo manifest,
|
|
50
|
+
* which is compiled into the IPA), and `targets.ios.ownershipScope` additions. Accepts the raw or the expanded target.
|
|
51
|
+
* The scan itself closes this scope under local path dependencies of the in-scope manifests (see ownership.mjs).
|
|
52
|
+
* A native assembly rooted at the repository root is ignored (it would make the scope the whole repository).
|
|
53
|
+
* Returns null when the target has no iosPackage (nothing to scope from).
|
|
54
|
+
*/
|
|
55
|
+
export function iosOwnershipScope({ repoRoot, appRoot, target, nativeAssembly }) {
|
|
56
|
+
const spec = target?.recipe?.kind === "iosPackage" ? target.recipe.spec : target?.iosPackage;
|
|
57
|
+
if (!spec || typeof spec !== "object") return null;
|
|
58
|
+
const toRepo = (absolute) => {
|
|
59
|
+
const relative = path.relative(repoRoot, absolute).split(path.sep).join("/");
|
|
60
|
+
if (relative === ".." || relative.startsWith("../") || path.isAbsolute(relative)) throw new Error(`ios ownership scope: ${absolute} is outside the repository`);
|
|
61
|
+
return relative || ".";
|
|
62
|
+
};
|
|
63
|
+
const specFile = spec.projectSpec ?? spec.project;
|
|
64
|
+
if (typeof specFile !== "string") throw new Error("ios ownership scope: iosPackage.project or projectSpec is required");
|
|
65
|
+
const specDir = path.resolve(appRoot, path.posix.dirname(specFile.replaceAll("\\", "/")));
|
|
66
|
+
const roots = [];
|
|
67
|
+
const projectDir = toRepo(specDir);
|
|
68
|
+
if (projectDir === ".") throw new Error("ios ownership scope: the Xcode project sits at the repository root, so there is no narrower scope; declare the project in a subdirectory");
|
|
69
|
+
roots.push(projectDir);
|
|
70
|
+
if (spec.projectSpec) {
|
|
71
|
+
const ymlPath = path.resolve(appRoot, spec.projectSpec);
|
|
72
|
+
if (existsSync(ymlPath)) {
|
|
73
|
+
for (const local of projectYmlLocalPackagePaths(readFileSync(ymlPath, "utf8"))) roots.push(toRepo(path.resolve(specDir, local)));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const native = nativeAssembly && typeof nativeAssembly === "object" ? nativeAssembly : null;
|
|
77
|
+
const nativeDirs = [native?.workspaceRoot ?? native?.cargoWorkspaceRoot, (native?.cargoManifest ?? native?.manifest) ? path.dirname(native.cargoManifest ?? native.manifest) : undefined].filter((value) => typeof value === "string" && value);
|
|
78
|
+
for (const dir of nativeDirs) {
|
|
79
|
+
const nativeRoot = toRepo(path.resolve(appRoot, dir));
|
|
80
|
+
// A native assembly at the repository root is the app's own (usually desktop) workspace; folding it in would widen the
|
|
81
|
+
// scope to the whole repository, so it adds nothing. The scan's path-dependency closure still follows the in-scope manifests.
|
|
82
|
+
if (nativeRoot !== ".") roots.push(nativeRoot);
|
|
83
|
+
}
|
|
84
|
+
const extra = target.ownershipScope;
|
|
85
|
+
if (extra !== undefined) roots.push(...normalizeScopeRoots(extra, { repoRoot, label: "targets.ios.ownershipScope" }));
|
|
86
|
+
const checked = normalizeScopeRoots(roots.filter((root) => root !== OWNERSHIP_SCOPE_ALWAYS), { repoRoot, label: "ios ownership scope" });
|
|
87
|
+
return [...new Set([...checked, OWNERSHIP_SCOPE_ALWAYS])].sort();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Repository-relative file or directory under any scope root (null scope = everything). */
|
|
91
|
+
export function inOwnershipScope(scope, rel) {
|
|
92
|
+
if (!scope) return true;
|
|
93
|
+
return scope.some((root) => rel === root || rel.startsWith(`${root}/`));
|
|
94
|
+
}
|
package/ownership.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import { createHash } from "node:crypto";
|
|
|
12
12
|
import { acquireCommittedSource, committedSourceDataForInternalUse } from "./committed-source.mjs";
|
|
13
13
|
import { readCommittedKeyAuthority } from "./ownership-key-authority.mjs";
|
|
14
14
|
import { gitListedInventory } from "./ownership-git-inventory.mjs";
|
|
15
|
+
import { inOwnershipScope } from "./ownership-scope.mjs";
|
|
15
16
|
import os from "node:os";
|
|
16
17
|
import path from "node:path";
|
|
17
18
|
|
|
@@ -156,7 +157,31 @@ function resolveLocalPath(root, sourceFile, pathValue, coverage, display, invent
|
|
|
156
157
|
} catch { coverage.push({ path: display, status: "unscanned", reason: `${reasonPrefix}-path-unresolved${detail ? `:${detail}` : ""}` }); }
|
|
157
158
|
}
|
|
158
159
|
|
|
159
|
-
|
|
160
|
+
// A Git dependency pinned by a full 40-hex `rev` is immutable. When its URL is third-party it is equivalent to a registry
|
|
161
|
+
// dependency and is not a gap (Orthic-Labs/rightkit#677). A first-party URL (Orthic-Labs, bogusyogi, or an org listed in
|
|
162
|
+
// ownership-repositories.json) stays an unscanned fatal gap because first-party code must be scanned. A branch, a tag, a
|
|
163
|
+
// short rev or no rev is always an unscanned coverage gap.
|
|
164
|
+
const FULL_GIT_REV = /^[0-9a-f]{40}$/i;
|
|
165
|
+
export const PINNED_GIT_DEPENDENCY = "pinned-git-dependency";
|
|
166
|
+
const FIRST_PARTY_GIT_ORGS = new Set(["github.com/orthic-labs", "github.com/bogusyogi"]);
|
|
167
|
+
const gitHostOrg = (url) => {
|
|
168
|
+
const text = String(url).trim();
|
|
169
|
+
const match = /^(?:[a-z][a-z0-9+.-]*:\/\/)?(?:[^@/\s]+@)?([^/:\s]+)[/:]+([^/\s]+)/i.exec(text);
|
|
170
|
+
return match ? `${match[1]}/${match[2]}`.toLowerCase() : null;
|
|
171
|
+
};
|
|
172
|
+
let configuredFirstParty;
|
|
173
|
+
function firstPartyGitOrgs() {
|
|
174
|
+
if (configuredFirstParty) return configuredFirstParty;
|
|
175
|
+
const orgs = new Set(FIRST_PARTY_GIT_ORGS);
|
|
176
|
+
try {
|
|
177
|
+
const config = JSON.parse(readFileSync(new URL("./ownership-repositories.json", import.meta.url), "utf8"));
|
|
178
|
+
for (const repo of config.repositories ?? []) { const key = gitHostOrg(repo.repositoryIdentity ?? ""); if (key) orgs.add(key); }
|
|
179
|
+
} catch { /* the built-in first-party orgs still apply */ }
|
|
180
|
+
return (configuredFirstParty = orgs);
|
|
181
|
+
}
|
|
182
|
+
export const isFirstPartyGitUrl = (url) => { const key = gitHostOrg(url); return key === null || firstPartyGitOrgs().has(key); };
|
|
183
|
+
|
|
184
|
+
function parseCargo(parsed, file, out, workspace, root, coverage, inventory, covered = []) {
|
|
160
185
|
const pathSpec = (pathValue, sourceFile, display) => {
|
|
161
186
|
resolveLocalPath(root, sourceFile, pathValue, coverage, display, inventory);
|
|
162
187
|
};
|
|
@@ -176,7 +201,15 @@ function parseCargo(parsed, file, out, workspace, root, coverage, inventory) {
|
|
|
176
201
|
if (!raw || typeof raw !== "string" && (typeof raw !== "object" || Array.isArray(raw)) || features.some((x) => typeof x !== "string") || requirement !== null && typeof requirement !== "string") throw new Error(`malformed dependency declaration: ${alias}`);
|
|
177
202
|
addDependency(out, "cargo", name, file, { alias: name === alias ? null : alias, section: `${prefix}${section}`, requirement, features, defaultFeatures: spec["default-features"] ?? inherited["default-features"] ?? null, source, resolvedVersion: null, local: !!pathValue || !!git });
|
|
178
203
|
if (pathValue) pathSpec(pathValue, spec.path ? file : path.posix.join(workspace.dir ?? ".", "Cargo.toml"), file);
|
|
179
|
-
if (git)
|
|
204
|
+
if (git) {
|
|
205
|
+
const rev = spec.rev ?? inherited.rev;
|
|
206
|
+
const moving = [spec.branch, inherited.branch, spec.tag, inherited.tag].some((value) => value !== undefined);
|
|
207
|
+
if (typeof git === "string" && typeof rev === "string" && FULL_GIT_REV.test(rev) && !moving && !isFirstPartyGitUrl(git)) {
|
|
208
|
+
// Immutable third-party source, not a gap: kept out of `coverage` (every coverage row blocks the gate) and reported on
|
|
209
|
+
// `deps.covered`. `scanned: false` records that the source is pinned, not examined.
|
|
210
|
+
covered.push({ path: file, status: PINNED_GIT_DEPENDENCY, reason: `git-dependency-source:${name}`, fingerprint: `${git}@${rev.toLowerCase()}`, scanned: false });
|
|
211
|
+
} else coverage.push({ path: file, status: "unscanned", reason: `git-dependency-source:${name}` });
|
|
212
|
+
}
|
|
180
213
|
}
|
|
181
214
|
for (const [target, value] of Object.entries(table.target ?? {})) addTable(value, `${prefix}target.'${target}'.`);
|
|
182
215
|
};
|
|
@@ -208,14 +241,40 @@ function fileInventory(appRoot, trust, pathPrefix = "", repositoryRoot = appRoot
|
|
|
208
241
|
return listedCoverage(inventory);
|
|
209
242
|
}
|
|
210
243
|
|
|
211
|
-
|
|
244
|
+
// A scoped scan also reads the workspace/lockfile authorities that sit in an ancestor directory of a scope root
|
|
245
|
+
// (a root Cargo.toml / Cargo.lock / lockfile resolves a member crate's inherited fields and lock graph), so a member
|
|
246
|
+
// of a larger workspace is judged against the same lockfile as in a whole-repository scan.
|
|
247
|
+
function inScopeOrAuthority(scope, rel) {
|
|
248
|
+
if (inOwnershipScope(scope, rel)) return true;
|
|
249
|
+
const dir = path.posix.dirname(rel);
|
|
250
|
+
return ["Cargo.toml", "Cargo.lock", "package.json", "pnpm-workspace.yaml", ...OWNERSHIP_LOCKFILES].includes(path.posix.basename(rel))
|
|
251
|
+
&& scope.some((root) => dir === "." || root === dir || root.startsWith(`${dir}/`));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Directories named by local path dependencies of in-scope manifests that are not yet in scope. A target that is not a
|
|
255
|
+
// directory of the committed tree is left to resolveLocalPath, which records a blocking coverage gap for it.
|
|
256
|
+
function localDependencyRoots(deps, scope, entries) {
|
|
257
|
+
const roots = new Set();
|
|
258
|
+
for (const dep of deps) {
|
|
259
|
+
if (!dep.local || typeof dep.source !== "string" || !dep.source.startsWith("path:")) continue;
|
|
260
|
+
const value = dep.source.slice(5);
|
|
261
|
+
if (!value || value.startsWith("workspace:") || path.posix.isAbsolute(value) || path.win32.isAbsolute(value) || value.includes("\\")) continue;
|
|
262
|
+
const relative = path.posix.normalize(path.posix.join(path.posix.dirname(dep.file), value)).replace(/\/$/, "");
|
|
263
|
+
if (relative === "." || relative === ".." || relative.startsWith("../") || inOwnershipScope(scope, relative)) continue;
|
|
264
|
+
if (entries.some((entry) => entry.rel.startsWith(`${relative}/`))) roots.add(relative);
|
|
265
|
+
}
|
|
266
|
+
return [...roots];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function committedFileInventory(appRoot, trust, pathPrefix, committed, scope = null) {
|
|
212
270
|
const prefix = pathPrefix ? `${pathPrefix.replace(/\/$/, "")}/` : "";
|
|
213
271
|
const repositoryFiles = committed.entries.filter((entry) => entry.type === "blob").map((entry) => ({ rel: entry.rel, size: entry.size, bytes: entry.bytes, mode: entry.mode }));
|
|
214
|
-
const files = [], coverage = committed.coverage.filter((entry) => !prefix || entry.path.startsWith(prefix)).map((entry) => ({ ...entry, path: prefix ? entry.path.slice(prefix.length) : entry.path }));
|
|
272
|
+
const files = [], coverage = committed.coverage.filter((entry) => (!prefix || entry.path.startsWith(prefix)) && inScopeOrAuthority(scope, entry.path)).map((entry) => ({ ...entry, path: prefix ? entry.path.slice(prefix.length) : entry.path }));
|
|
215
273
|
const protectedPaths = new Set((trust?.provenance ?? []).flatMap((record) => record.paths ?? []));
|
|
216
274
|
const reportedOpaque = new Set(), reportedNodeModules = new Set();
|
|
217
275
|
for (const entry of committed.entries) {
|
|
218
276
|
if (prefix && !entry.rel.startsWith(prefix)) continue;
|
|
277
|
+
if (!inScopeOrAuthority(scope, entry.rel)) continue;
|
|
219
278
|
const rel = prefix ? entry.rel.slice(prefix.length) : entry.rel;
|
|
220
279
|
if (!rel) continue;
|
|
221
280
|
if (entry.mode === "120000") { coverage.push({ path: rel, status: "unscanned", reason: "committed-symlink" }); continue; }
|
|
@@ -375,7 +434,7 @@ function resolveDependencyVersions(appRoot, inventory, deps, coverage, contentDi
|
|
|
375
434
|
}
|
|
376
435
|
|
|
377
436
|
function dependenciesFromInventory(appRoot, inventory) {
|
|
378
|
-
const deps = [], coverage = [], contentDigests = [];
|
|
437
|
+
const deps = [], coverage = [], contentDigests = [], covered = [];
|
|
379
438
|
// Workspace inheritance is loaded before member manifests.
|
|
380
439
|
const manifestNames = new Set(["Cargo.toml", "package.json", "Package.swift"]);
|
|
381
440
|
const manifests = inventory.files.filter((x) => manifestNames.has(path.posix.basename(x.rel)));
|
|
@@ -419,7 +478,7 @@ function dependenciesFromInventory(appRoot, inventory) {
|
|
|
419
478
|
if (base === "Cargo.toml") {
|
|
420
479
|
const dir = path.posix.dirname(rel);
|
|
421
480
|
const workspace = workspaces.filter((x) => x.dir === "." || dir === x.dir || dir.startsWith(`${x.dir}/`)).sort((a, b) => b.dir.length - a.dir.length)[0] ?? { dir: ".", deps: {} };
|
|
422
|
-
try { parseCargo(parseToml(text), rel, deps, workspace, appRoot, coverage, inventory); }
|
|
481
|
+
try { parseCargo(parseToml(text), rel, deps, workspace, appRoot, coverage, inventory, covered); }
|
|
423
482
|
catch { coverage.push({ path: rel, status: "unscanned", reason: "invalid-cargo-toml" }); }
|
|
424
483
|
}
|
|
425
484
|
else if (base === "package.json") {
|
|
@@ -453,6 +512,7 @@ function dependenciesFromInventory(appRoot, inventory) {
|
|
|
453
512
|
resolveDependencyVersions(appRoot, inventory, deps, coverage, contentDigests);
|
|
454
513
|
Object.defineProperty(deps, "coverage", { value: coverage, enumerable: false });
|
|
455
514
|
Object.defineProperty(deps, "contentDigests", { value: contentDigests, enumerable: false });
|
|
515
|
+
Object.defineProperty(deps, "covered", { value: covered, enumerable: false });
|
|
456
516
|
Object.defineProperty(deps, "manifests", { value: files, enumerable: false });
|
|
457
517
|
return deps;
|
|
458
518
|
}
|
|
@@ -801,14 +861,28 @@ function checkCommitted(appRoot, manifest, options, acquire, readAuthority = rea
|
|
|
801
861
|
keyCheckpoint = resolved.checkpoint;
|
|
802
862
|
}
|
|
803
863
|
const location = { root: committed.repositoryRoot, prefix: pathPrefix, pathBase: "repository" };
|
|
804
|
-
const
|
|
805
|
-
if (location.prefix && !committed.entries.some((entry) => entry.rel.startsWith(location.prefix))) {
|
|
806
|
-
inventory.coverage.push({ path: ".", status: "unscanned", reason: "registered-root-missing-from-committed-tree" });
|
|
807
|
-
}
|
|
864
|
+
const declaredScope = options.scope ?? null;
|
|
808
865
|
const committedEnrollment = trust?.rootFingerprint
|
|
809
866
|
? { enrolled: trust.enrollmentComplete === true, pending: trust.enrollmentComplete !== true }
|
|
810
867
|
: trustState?.pending !== undefined || trustState?.enrolled !== undefined ? trustState : undefined;
|
|
811
|
-
|
|
868
|
+
// The scope is closed under local path dependencies: a path dependency of an in-scope manifest that lands in an
|
|
869
|
+
// out-of-scope directory adds that directory to the scope and the scan repeats, so compiled-in code is never skipped.
|
|
870
|
+
let scope = declaredScope ? [...declaredScope] : null;
|
|
871
|
+
let inventory, scanned;
|
|
872
|
+
for (let round = 0; ; round += 1) {
|
|
873
|
+
inventory = committedFileInventory(appRoot, trust, location.prefix, committed, scope);
|
|
874
|
+
if (location.prefix && !committed.entries.some((entry) => entry.rel.startsWith(location.prefix))) {
|
|
875
|
+
inventory.coverage.push({ path: ".", status: "unscanned", reason: "registered-root-missing-from-committed-tree" });
|
|
876
|
+
}
|
|
877
|
+
scanned = scanInternal(appRoot, manifest, { inventory, trust, repositoryIdentity, repositoryRoot: location.root, pathPrefix: location.prefix, allowlistPolicy: dependencyAllowlistPolicy(trust, repositoryIdentity, { pending: enrollmentPending(trust, committedEnrollment), shipped: options.shippedAllowlist }) });
|
|
878
|
+
if (!scope) break;
|
|
879
|
+
const added = localDependencyRoots(scanned.deps, scope, committed.entries);
|
|
880
|
+
if (!added.length) break;
|
|
881
|
+
if (round >= 64) throw new Error("committed ownership: scan scope did not converge under local path dependencies");
|
|
882
|
+
scope = [...new Set([...scope, ...added])].sort();
|
|
883
|
+
}
|
|
884
|
+
// Provenance and lockfile diagnostics are computed beside the inventory; a scoped scan reports only gaps under its scope.
|
|
885
|
+
if (scope) scanned.coverage = scanned.coverage.filter((entry) => typeof entry.path !== "string" || entry.path === "." || inScopeOrAuthority(scope, entry.path));
|
|
812
886
|
const exceptionPath = [location.prefix.replace(/\/$/, ""), EXCEPTIONS_FILE.split(path.sep).join("/")].filter(Boolean).join("/");
|
|
813
887
|
const exceptionObject = committed.entries.find((entry) => entry.rel === exceptionPath);
|
|
814
888
|
const exceptionEntry = exceptionObject?.bytes;
|
|
@@ -823,6 +897,7 @@ function checkCommitted(appRoot, manifest, options, acquire, readAuthority = rea
|
|
|
823
897
|
scannedAt: new Date().toISOString(), catalogDigest: catalogDigest(manifest), catalogVersion: manifest.version ?? String(manifest.schema ?? "1"),
|
|
824
898
|
scannerVersion: SCANNER_VERSION, contentDigests: scanned.contentDigests, coverage: scanned.coverage,
|
|
825
899
|
pathBase: location.pathBase, pathPrefix: location.prefix, provenance: scanned.provenance, exceptionDigest,
|
|
900
|
+
...(scope ? { ownershipScopeClosed: scope } : {}),
|
|
826
901
|
source: "committed-git-tree", ...(keyLogDigest ? { keyLogDigest, keyCheckpoint, rootFingerprint: trust.rootFingerprint, ...(keyAuthorityBinding ? { keyAuthority: keyAuthorityBinding } : {}) } : {}),
|
|
827
902
|
};
|
|
828
903
|
const result = evaluate(scanned.findings, exceptions, { ...options, manifest, trust, trustEnrollment: trust?.rootFingerprint
|
|
@@ -9,6 +9,8 @@ import path from "node:path";
|
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
import { resolveTargetRoot as cargoResolveTargetRoot } from "./cargo-target.mjs";
|
|
11
11
|
import { validateWinPackage, WinPackageConfigError } from "./win-package-recipe.mjs";
|
|
12
|
+
import { IOS_SIGNING_CONTRACT, IosPackageError, validateIosPackageConfig } from "./ios-package-recipe.mjs";
|
|
13
|
+
import { IosUploadError, validateIosDistribution } from "./ios-upload.mjs";
|
|
12
14
|
|
|
13
15
|
export class PackageRecipeTargetError extends Error {
|
|
14
16
|
constructor(message) { super(message); this.name = "PackageRecipeTargetError"; this.code = "RK_PACKAGE_RECIPE_TARGET"; }
|
|
@@ -25,6 +27,7 @@ const withoutRecipe = (target) => {
|
|
|
25
27
|
};
|
|
26
28
|
const QUIET_TAURI_ENV = { TAURI_SIGNING_PRIVATE_KEY: "", TAURI_SIGNING_PRIVATE_KEY_PASSWORD: "" };
|
|
27
29
|
const RECIPE_BUILD = fileURLToPath(new URL("./recipe-build.mjs", import.meta.url));
|
|
30
|
+
const CREATE_MAC_UPDATER = fileURLToPath(new URL("./create-mac-updater.mjs", import.meta.url));
|
|
28
31
|
const PROFILE = /^[a-z][a-z0-9_-]*$/;
|
|
29
32
|
|
|
30
33
|
function relativeInside(value, label) {
|
|
@@ -160,7 +163,7 @@ export function expandWinPackageTarget(target, { appRoot, config = {}, env = pro
|
|
|
160
163
|
};
|
|
161
164
|
}
|
|
162
165
|
|
|
163
|
-
const MAC_KEYS = new Set(["universal", "triple", "tauriCwd", "features", "profile", "tauriConfig", "prepare", "postBuild", "cleanup", "cargoTargetRoot"]);
|
|
166
|
+
const MAC_KEYS = new Set(["universal", "triple", "tauriCwd", "features", "profile", "tauriConfig", "prepare", "postBuild", "cleanup", "cargoTargetRoot", "updater"]);
|
|
164
167
|
const MAC_TRIPLES = ["aarch64-apple-darwin", "x86_64-apple-darwin"];
|
|
165
168
|
const MAC_CLEANUP_KEYS = new Set(["staleApps", "staleDmgVolumes", "lsclean"]);
|
|
166
169
|
|
|
@@ -179,6 +182,8 @@ function macCleanupSpec(value) {
|
|
|
179
182
|
* The app path and DMG path are `notarize.app` and `notarize.file`; DMG branding is `targets.mac.dmg`.
|
|
180
183
|
* Architecture: `triple` (an explicit arch triple) beats `universal`; `universal: false` means this host's arch.
|
|
181
184
|
* `cleanup` runs built-in steps in recipe-build before the build (stale DMG volumes, stale .app bundles, lsregister).
|
|
185
|
+
* `updater: true | "<app-relative .tar.gz>"` emits the Tauri updater tarball and its signature from the signed, stapled .app:
|
|
186
|
+
* it prepends a `create-mac-updater` command to `postSign` (default output `<App>.app.tar.gz` beside the app's working root).
|
|
182
187
|
*/
|
|
183
188
|
export function expandMacPackageTarget(target, { appRoot, env = process.env, resolveTargetRoot = cargoResolveTargetRoot }) {
|
|
184
189
|
if (!target?.macPackage) return withoutRecipe(target);
|
|
@@ -196,6 +201,8 @@ export function expandMacPackageTarget(target, { appRoot, env = process.env, res
|
|
|
196
201
|
const cwd = spec.tauriCwd ?? ".";
|
|
197
202
|
if (typeof cwd !== "string" || path.isAbsolute(cwd) || cwd.split(/[\\/]/).includes("..")) fail("targets.mac.macPackage.tauriCwd must be a relative path inside the app");
|
|
198
203
|
const cleanupSpec = macCleanupSpec(spec.cleanup);
|
|
204
|
+
if (spec.updater !== undefined && spec.updater !== true && spec.updater !== false && typeof spec.updater !== "string") fail("targets.mac.macPackage.updater must be true or an app-relative .tar.gz path");
|
|
205
|
+
if (typeof spec.updater === "string" && !/\.tar\.gz$/i.test(relativeInside(spec.updater, "targets.mac.macPackage.updater"))) fail("targets.mac.macPackage.updater must end in .tar.gz");
|
|
199
206
|
// Tauri's universal-apple-darwin target builds both slices and lipos them itself.
|
|
200
207
|
const triple = spec.triple ?? (spec.universal === false ? (process.arch === "arm64" ? "aarch64-apple-darwin" : "x86_64-apple-darwin") : "universal-apple-darwin");
|
|
201
208
|
if (spec.triple !== undefined) {
|
|
@@ -225,8 +232,17 @@ export function expandMacPackageTarget(target, { appRoot, env = process.env, res
|
|
|
225
232
|
: undefined;
|
|
226
233
|
const wrapped = extras.prepare.length || extras.postBuild.length || cleanup;
|
|
227
234
|
const { macPackage, recipe: _ignoredMac, ...rest } = target;
|
|
235
|
+
// The updater tarball is made from the signed .app, so it runs in postSign, ahead of any postSign the app declares.
|
|
236
|
+
const updaterOutput = spec.updater === true ? `${productName}.app.tar.gz` : typeof spec.updater === "string" ? relativeInside(spec.updater, "targets.mac.macPackage.updater") : undefined;
|
|
237
|
+
const postSign = updaterOutput === undefined ? {} : {
|
|
238
|
+
postSign: [
|
|
239
|
+
{ cmd: process.execPath, args: [CREATE_MAC_UPDATER, "--app", app, "--output", updaterOutput] },
|
|
240
|
+
...(target.postSign === undefined ? [] : Array.isArray(target.postSign) ? target.postSign : [target.postSign]),
|
|
241
|
+
],
|
|
242
|
+
};
|
|
228
243
|
return {
|
|
229
244
|
...rest,
|
|
245
|
+
...postSign,
|
|
230
246
|
prePackage: wrapped ? recipeBuildCommand({ extras, build: { cmd: "pnpm", args, cwd }, env: QUIET_TAURI_ENV, cleanup }) : { cmd: "pnpm", args, cwd, env: QUIET_TAURI_ENV },
|
|
231
247
|
sign: { ...(target.sign ?? {}), prePackageFiles },
|
|
232
248
|
notarize: { ...target.notarize, app, appBeforePackage: true },
|
|
@@ -235,11 +251,49 @@ export function expandMacPackageTarget(target, { appRoot, env = process.env, res
|
|
|
235
251
|
};
|
|
236
252
|
}
|
|
237
253
|
|
|
254
|
+
const IOS_TARGET_KEYS = new Set(["signed", "signingContract", "iosPackage", "artifacts", "hardening", "hardeningAllowances", "qaControlScan", "publishBlocked", "distribution", "ownershipScope", "cargoManifest", "cargoTarget", "recipe"]);
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* `targets.ios.iosPackage` (bogusyogi/rightkit#349) is validated strictly and kept as `recipe: { kind: "iosPackage", spec }`.
|
|
258
|
+
* Unlike macPackage there is no hook to expand into: xcodebuild archive and export run in ios-package-recipe.mjs (macOS only),
|
|
259
|
+
* which release.mjs calls like the Android lane. The target may carry only the keys in IOS_TARGET_KEYS: a hand-written
|
|
260
|
+
* prePackage / package / sign / notarize / gradle script, or an App Store Connect credential, is refused.
|
|
261
|
+
* `artifacts` must list the .ipa so the IPA is checked and sealed. `distribution` (the App Store Connect target of an app that
|
|
262
|
+
* also ships desktop) is validated here exactly as the upload validates it, so a credential in it fails the build.
|
|
263
|
+
*/
|
|
264
|
+
export function expandIosPackageTarget(target, { appRoot: _appRoot } = {}) {
|
|
265
|
+
if (!isObject(target) || target.iosPackage === undefined) return withoutRecipe(target);
|
|
266
|
+
const unknown = Object.keys(target).filter((key) => !IOS_TARGET_KEYS.has(key));
|
|
267
|
+
if (unknown.length) fail(`targets.ios has unknown key(s): ${unknown.join(", ")} (allowed: ${[...IOS_TARGET_KEYS].filter((key) => key !== "recipe").join(", ")}); the iOS recipe replaces prePackage, package, sign and notarize scripts, and App Store Connect credentials never belong in a target`);
|
|
268
|
+
if (target.distribution !== undefined) {
|
|
269
|
+
try { validateIosDistribution(target.distribution); } catch (error) {
|
|
270
|
+
if (error instanceof IosUploadError) fail(`targets.ios.${error.message.replace(/^\[[A-Z0-9_]+\] /, "")}`);
|
|
271
|
+
throw error;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (target.ownershipScope !== undefined && (!Array.isArray(target.ownershipScope) || target.ownershipScope.some((entry) => typeof entry !== "string" || !entry.trim()))) fail("targets.ios.ownershipScope must be an array of repository-relative directory strings");
|
|
275
|
+
// The Rust the app links (for the feature-closure scan): an app-relative Cargo.toml and its iPhone triple.
|
|
276
|
+
if (target.cargoManifest !== undefined && (typeof target.cargoManifest !== "string" || !target.cargoManifest.endsWith("Cargo.toml"))) fail("targets.ios.cargoManifest must be an app-relative path to a Cargo.toml");
|
|
277
|
+
if (target.cargoTarget !== undefined && !/^aarch64-apple-ios(-sim)?$/.test(target.cargoTarget)) fail("targets.ios.cargoTarget must be aarch64-apple-ios or aarch64-apple-ios-sim");
|
|
278
|
+
if (target.signed !== true) fail("targets.ios must declare signed: true");
|
|
279
|
+
if (target.signingContract !== undefined && target.signingContract !== IOS_SIGNING_CONTRACT) fail(`targets.ios.iosPackage signs with Xcode automatic signing; signingContract must be ${IOS_SIGNING_CONTRACT}, got ${target.signingContract}`);
|
|
280
|
+
let spec;
|
|
281
|
+
try { spec = validateIosPackageConfig(target.iosPackage); } catch (error) {
|
|
282
|
+
if (error instanceof IosPackageError) fail(`targets.ios.${error.message}`);
|
|
283
|
+
throw error;
|
|
284
|
+
}
|
|
285
|
+
const declared = (target.artifacts ?? []).map((artifact) => String(typeof artifact === "string" ? artifact : artifact?.file).replaceAll("\\", "/").replace(/^\.\//, ""));
|
|
286
|
+
if (!declared.includes(spec.ipa)) fail("targets.ios.artifacts must list iosPackage.ipa so the IPA is checked and sealed");
|
|
287
|
+
const { iosPackage, recipe: _ignored, ...rest } = target;
|
|
288
|
+
return { ...rest, signingContract: IOS_SIGNING_CONTRACT, recipe: { kind: "iosPackage", spec } };
|
|
289
|
+
}
|
|
290
|
+
|
|
238
291
|
/** Expand whichever declarative package key the target carries. */
|
|
239
292
|
export function expandPackageRecipeTarget(target, { platform, appRoot, config, env, resolveTargetRoot }) {
|
|
240
293
|
const injected = { ...(env ? { env } : {}), ...(resolveTargetRoot ? { resolveTargetRoot } : {}) };
|
|
241
294
|
if (platform === "win") return expandWinPackageTarget(target, { appRoot, config, ...injected });
|
|
242
295
|
if (platform === "mac") return expandMacPackageTarget(target, { appRoot, config, ...injected });
|
|
243
|
-
if (
|
|
296
|
+
if (platform === "ios") return expandIosPackageTarget(target, { appRoot });
|
|
297
|
+
if (target?.winPackage || target?.macPackage || target?.iosPackage) fail(`winPackage / macPackage / iosPackage are not valid on ${platform}`);
|
|
244
298
|
return target;
|
|
245
299
|
}
|
package/package.json
CHANGED
package/pe-content.mjs
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Signature-independent PE content digest and import-closure check (bogusyogi/rightkit#445).
|
|
3
|
+
// right-release pe digest <file> [--algorithm sha256|sha384|sha512] [--json]
|
|
4
|
+
// right-release pe imports <file> [--allow <dll[,dll...]>]... [--allow-file <path>] [--json]
|
|
5
|
+
// Pure Buffer parsing, so both run on any host. The digest follows the Authenticode image hash: the CheckSum field,
|
|
6
|
+
// the Certificate Table directory entry and the certificate data itself are excluded, so re-signing or timestamping
|
|
7
|
+
// a binary never changes it.
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import { readFileSync } from "node:fs";
|
|
10
|
+
import { readPeImports } from "./pe-imports.mjs";
|
|
11
|
+
|
|
12
|
+
const ALGORITHMS = new Set(["sha256", "sha384", "sha512"]);
|
|
13
|
+
function fail(message, code = "PE_INVALID") { throw Object.assign(new Error(message), { code }); }
|
|
14
|
+
|
|
15
|
+
function layout(buf) {
|
|
16
|
+
if (buf.length < 0x40 || buf.readUInt16LE(0) !== 0x5a4d) fail("not a PE file (missing MZ)");
|
|
17
|
+
const pe = buf.readUInt32LE(0x3c);
|
|
18
|
+
if (pe + 24 > buf.length || buf.readUInt32LE(pe) !== 0x00004550) fail("not a PE file (missing PE signature)");
|
|
19
|
+
const sectionCount = buf.readUInt16LE(pe + 6);
|
|
20
|
+
const optionalSize = buf.readUInt16LE(pe + 20);
|
|
21
|
+
const optional = pe + 24;
|
|
22
|
+
const magic = buf.readUInt16LE(optional);
|
|
23
|
+
if (magic !== 0x20b && magic !== 0x10b) fail("unsupported PE optional header");
|
|
24
|
+
const directories = optional + (magic === 0x20b ? 112 : 96);
|
|
25
|
+
const numberOfDirectories = buf.readUInt32LE(directories - 4);
|
|
26
|
+
const checksumAt = optional + 64;
|
|
27
|
+
const sizeOfHeaders = buf.readUInt32LE(optional + 60);
|
|
28
|
+
const certEntryAt = directories + 4 * 8;
|
|
29
|
+
const hasCert = numberOfDirectories > 4;
|
|
30
|
+
if (checksumAt + 4 > buf.length || sizeOfHeaders > buf.length || (hasCert && certEntryAt + 8 > buf.length)) fail("truncated PE headers");
|
|
31
|
+
const sectionsAt = optional + optionalSize;
|
|
32
|
+
const sections = [];
|
|
33
|
+
for (let i = 0; i < sectionCount; i++) {
|
|
34
|
+
const at = sectionsAt + i * 40;
|
|
35
|
+
if (at + 40 > buf.length) fail("truncated PE section table");
|
|
36
|
+
const rawSize = buf.readUInt32LE(at + 16);
|
|
37
|
+
const rawPtr = buf.readUInt32LE(at + 20);
|
|
38
|
+
if (rawSize > 0 && rawPtr + rawSize > buf.length) fail("PE section extends past the end of the file");
|
|
39
|
+
sections.push({ rawSize, rawPtr });
|
|
40
|
+
}
|
|
41
|
+
// The certificate table directory holds a FILE OFFSET (not an RVA).
|
|
42
|
+
const cert = hasCert ? { offset: buf.readUInt32LE(certEntryAt), size: buf.readUInt32LE(certEntryAt + 4) } : { offset: 0, size: 0 };
|
|
43
|
+
if (cert.size === 0 && cert.offset !== 0) fail("PE certificate table has an offset but no size");
|
|
44
|
+
if (cert.size > 0) {
|
|
45
|
+
if (cert.offset + cert.size > buf.length) fail("PE certificate table extends past the end of the file");
|
|
46
|
+
// The certificate table must be the terminal region: bytes after it would otherwise escape the digest.
|
|
47
|
+
if (cert.offset + cert.size !== buf.length) fail("PE certificate table is not the terminal region of the file (bytes follow it)");
|
|
48
|
+
if (cert.offset < sizeOfHeaders) fail("PE certificate table overlaps the headers");
|
|
49
|
+
for (const s of sections) if (s.rawSize > 0 && s.rawPtr + s.rawSize > cert.offset) fail("PE certificate table overlaps a section");
|
|
50
|
+
}
|
|
51
|
+
return { checksumAt, sizeOfHeaders, certEntryAt, hasCert, sections, cert };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Authenticode-style SHA-2 digest of a PE image, independent of its signature, timestamp and CheckSum. */
|
|
55
|
+
export function peContentDigest(bytes, { algorithm = "sha256" } = {}) {
|
|
56
|
+
if (!ALGORITHMS.has(algorithm)) fail(`unsupported digest algorithm: ${algorithm}`, "PE_USAGE");
|
|
57
|
+
const buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes);
|
|
58
|
+
const l = layout(buf);
|
|
59
|
+
const hash = createHash(algorithm);
|
|
60
|
+
hash.update(buf.subarray(0, l.checksumAt));
|
|
61
|
+
if (l.hasCert) {
|
|
62
|
+
hash.update(buf.subarray(l.checksumAt + 4, l.certEntryAt));
|
|
63
|
+
hash.update(buf.subarray(l.certEntryAt + 8, l.sizeOfHeaders));
|
|
64
|
+
} else {
|
|
65
|
+
hash.update(buf.subarray(l.checksumAt + 4, l.sizeOfHeaders));
|
|
66
|
+
}
|
|
67
|
+
// Every byte after the headers up to the certificate table (or end of file) is hashed: sections, inter-section gaps and overlay.
|
|
68
|
+
const trailEnd = l.cert.size > 0 ? l.cert.offset : buf.length;
|
|
69
|
+
if (trailEnd > l.sizeOfHeaders) hash.update(buf.subarray(l.sizeOfHeaders, trailEnd));
|
|
70
|
+
return hash.digest("hex");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const normalize = (name) => String(name).trim().toLowerCase();
|
|
74
|
+
|
|
75
|
+
function allowMatcher(allowed) {
|
|
76
|
+
const entries = allowed.map(normalize).filter(Boolean);
|
|
77
|
+
return (dll) => entries.some((entry) => (entry.endsWith("*") ? dll.startsWith(entry.slice(0, -1)) : dll === entry));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check that every DLL a PE imports (regular and delay-load) is in `allowedDlls`. Names compare case-insensitively;
|
|
82
|
+
* a trailing `*` allows a prefix (`api-ms-win-*`). An empty allow list refuses every import.
|
|
83
|
+
* @returns {{ok: boolean, architecture: string, imports: string[], delayImports: string[], unexpected: string[]}}
|
|
84
|
+
*/
|
|
85
|
+
export function verifyImportClosure(bytes, allowedDlls) {
|
|
86
|
+
if (!Array.isArray(allowedDlls)) fail("allowedDlls must be an array", "PE_USAGE");
|
|
87
|
+
const info = readPeImports(bytes);
|
|
88
|
+
const allowed = allowMatcher(allowedDlls);
|
|
89
|
+
const all = [...new Set([...info.imports, ...info.delayImports].map(normalize))].sort();
|
|
90
|
+
const unexpected = all.filter((dll) => !allowed(dll));
|
|
91
|
+
return { ok: unexpected.length === 0, architecture: info.architecture, imports: info.imports, delayImports: info.delayImports, unexpected };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const USAGE = "usage: right-release pe digest <file> [--algorithm sha256|sha384|sha512] [--json] | pe imports <file> [--allow dll,...] [--allow-file <path>] [--json]";
|
|
95
|
+
|
|
96
|
+
function parseArgs(argv) {
|
|
97
|
+
const [sub, file, ...rest] = argv;
|
|
98
|
+
const opts = { sub, file, algorithm: "sha256", allow: [], json: false };
|
|
99
|
+
for (let i = 0; i < rest.length; i++) {
|
|
100
|
+
const arg = rest[i];
|
|
101
|
+
if (arg === "--json") opts.json = true;
|
|
102
|
+
else if (["--algorithm", "--allow", "--allow-file"].includes(arg)) {
|
|
103
|
+
const value = rest[++i];
|
|
104
|
+
if (value === undefined || value.startsWith("--")) fail(`${arg} needs a value`, "PE_USAGE");
|
|
105
|
+
if (arg === "--algorithm") opts.algorithm = value;
|
|
106
|
+
else if (arg === "--allow") opts.allow.push(...value.split(","));
|
|
107
|
+
else opts.allow.push(...readFileSync(value, "utf8").split(/\r?\n/).map((l) => l.replace(/#.*/, "").trim()).filter(Boolean));
|
|
108
|
+
} else fail(`unknown pe argument: ${arg}`, "PE_USAGE");
|
|
109
|
+
}
|
|
110
|
+
if (!["digest", "imports"].includes(sub) || !file) fail(USAGE, "PE_USAGE");
|
|
111
|
+
return opts;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function main(argv = process.argv.slice(2), out = console) {
|
|
115
|
+
let opts;
|
|
116
|
+
try { opts = parseArgs(argv); } catch (error) { out.error(error.message); return 2; }
|
|
117
|
+
try {
|
|
118
|
+
const bytes = readFileSync(opts.file);
|
|
119
|
+
if (opts.sub === "digest") {
|
|
120
|
+
const digest = peContentDigest(bytes, { algorithm: opts.algorithm });
|
|
121
|
+
out.log(opts.json ? JSON.stringify({ file: opts.file, algorithm: opts.algorithm, digest }) : `${opts.algorithm}:${digest}`);
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
const result = verifyImportClosure(bytes, opts.allow);
|
|
125
|
+
out.log(opts.json ? JSON.stringify({ file: opts.file, ...result }) : [`${opts.file} (${result.architecture})`, ...result.imports.map((d) => ` import ${d}`), ...result.delayImports.map((d) => ` delay-import ${d}`), ...result.unexpected.map((d) => ` UNEXPECTED ${d}`)].join("\n"));
|
|
126
|
+
return result.ok ? 0 : 1;
|
|
127
|
+
} catch (error) {
|
|
128
|
+
out.error(error.message);
|
|
129
|
+
return error.code === "PE_USAGE" ? 2 : 1;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (process.argv[1] && import.meta.url === new URL(`file://${process.argv[1]}`).href) process.exit(main());
|
package/preflight.mjs
CHANGED
|
@@ -192,15 +192,22 @@ export function collectPreflight({
|
|
|
192
192
|
isBrokerManagedHost = brokerManagedHost,
|
|
193
193
|
ownership = true,
|
|
194
194
|
ownershipEvidenceFactory = createOwnershipEvidence,
|
|
195
|
+
ownershipScope = null,
|
|
195
196
|
runtimeBudgetAdoption = appRuntimeBudgetAdoptionCheck,
|
|
196
197
|
freeDisk = freeDiskGb,
|
|
197
198
|
projectType,
|
|
199
|
+
// false for a native iOS app with no Cargo host: no target bridge, Rust compiler cache or Rust budget adoption applies.
|
|
200
|
+
rust = true,
|
|
201
|
+
// false skips only the target bridge and the sccache check (a native iOS app that pins a Cargo.lock has no broker target).
|
|
202
|
+
bridge = true,
|
|
198
203
|
msix = false,
|
|
199
204
|
findSdkTool = findWindowsSdkTool,
|
|
200
205
|
} = {}) {
|
|
201
206
|
const checks = [];
|
|
202
207
|
const windows = platform === "win" || platform === "win32";
|
|
203
208
|
const swift = isSwiftProject(projectType);
|
|
209
|
+
const noRust = swift || !rust;
|
|
210
|
+
const noBridge = noRust || !bridge;
|
|
204
211
|
|
|
205
212
|
if (swift) {
|
|
206
213
|
const swiftVersion = versionProbe("swift");
|
|
@@ -211,7 +218,7 @@ export function collectPreflight({
|
|
|
211
218
|
);
|
|
212
219
|
}
|
|
213
220
|
|
|
214
|
-
if (appRoot && !
|
|
221
|
+
if (appRoot && !noBridge) {
|
|
215
222
|
const target = targetLink ?? nativeLayout?.targetLink ?? path.join(appRoot, "src-tauri", "target");
|
|
216
223
|
const targetLabel = nativeLayout?.targetPrefix || "src-tauri/target/";
|
|
217
224
|
let entry = null;
|
|
@@ -273,7 +280,7 @@ export function collectPreflight({
|
|
|
273
280
|
else if (msix) checks.push(fail("signtool", "msix target configured but no signtool.exe found under any installed Windows SDK", signtoolMissing));
|
|
274
281
|
else checks.push(warn("signtool", "no signtool.exe found under any installed Windows SDK", signtoolMissing));
|
|
275
282
|
|
|
276
|
-
const vendorsOpenssl = !
|
|
283
|
+
const vendorsOpenssl = !noRust && cargoLockPaths.some((lockPath) => graphVendorsOpenssl(lockPath));
|
|
277
284
|
if (vendorsOpenssl) {
|
|
278
285
|
const perl = resolveWindowsPerl({ env });
|
|
279
286
|
checks.push(
|
|
@@ -288,8 +295,8 @@ export function collectPreflight({
|
|
|
288
295
|
}
|
|
289
296
|
}
|
|
290
297
|
|
|
291
|
-
if (
|
|
292
|
-
// No Rust compiler cache applies to SwiftPM targets.
|
|
298
|
+
if (noBridge) {
|
|
299
|
+
// No Rust compiler cache applies to SwiftPM targets or to a native iOS app without a Cargo host.
|
|
293
300
|
} else if (isBrokerManagedHost(env)) {
|
|
294
301
|
checks.push(ok("compiler-cache", "broker-managed; compiler-cache selection and status are recorded in build receipts"));
|
|
295
302
|
} else {
|
|
@@ -321,7 +328,7 @@ export function collectPreflight({
|
|
|
321
328
|
}
|
|
322
329
|
|
|
323
330
|
// Held-resource crates are proven at adoption: an app that links one needs its own installed measurement.
|
|
324
|
-
if (appRoot && !
|
|
331
|
+
if (appRoot && !noRust) {
|
|
325
332
|
const adoption = runtimeBudgetAdoption({ appRoot, app, version, cargoLockPaths });
|
|
326
333
|
if (adoption) checks.push(adoption);
|
|
327
334
|
}
|
|
@@ -329,7 +336,7 @@ export function collectPreflight({
|
|
|
329
336
|
if (repoRoot && ownership) {
|
|
330
337
|
// An app living inside the RightKit workspace repository is scoped to its own directory.
|
|
331
338
|
const workspace = existsSync(path.join(repoRoot, "tools", "rightkit", "packages", "release"));
|
|
332
|
-
checks.push(ownershipCheck(repoRoot, { scanRoot: workspace && appRoot ? appRoot : repoRoot, evidenceFactory: ownershipEvidenceFactory }));
|
|
339
|
+
checks.push(ownershipCheck(repoRoot, { scanRoot: workspace && appRoot ? appRoot : repoRoot, scope: ownershipScope, evidenceFactory: ownershipEvidenceFactory }));
|
|
333
340
|
}
|
|
334
341
|
|
|
335
342
|
return checks;
|
|
@@ -338,7 +345,7 @@ export function collectPreflight({
|
|
|
338
345
|
/** Release-blocking: the app keeps no unexcepted copy of a RightKit-owned capability. */
|
|
339
346
|
export function ownershipCheck(repoRoot, options = {}) {
|
|
340
347
|
let checked;
|
|
341
|
-
try { checked = (options.evidenceFactory ?? createOwnershipEvidence)(repoRoot, options.scanRoot ?? repoRoot); }
|
|
348
|
+
try { checked = (options.evidenceFactory ?? createOwnershipEvidence)(repoRoot, options.scanRoot ?? repoRoot, options.scope ?? null); }
|
|
342
349
|
catch (error) { return fail("ownership", `ownership evidence failed: ${error.message}`, "restore protected RightKit ownership trust and catalog"); }
|
|
343
350
|
const { result, evidence, floor } = checked;
|
|
344
351
|
if (checked.ok) {
|
package/project-type.mjs
CHANGED
|
@@ -29,7 +29,9 @@ export function resolveProjectType({ appRoot, config = {}, target = {}, exists =
|
|
|
29
29
|
export const isSwiftProject = (projectType) => projectType === "swiftpm";
|
|
30
30
|
|
|
31
31
|
/** Executables the build itself needs (the config's own preflight.executables are appended by callers). */
|
|
32
|
-
export function requiredBuildExecutables({ projectType, packageManager = "pnpm", usesPackageManager = true } = {}) {
|
|
32
|
+
export function requiredBuildExecutables({ projectType, packageManager = "pnpm", usesPackageManager = true, usesCargo = true } = {}) {
|
|
33
|
+
// A native iOS app with no Cargo host: git, plus the package manager only when the app has a package.json (xcodebuild/xcodegen are the recipe's own preflight).
|
|
34
|
+
if (!usesCargo && !isSwiftProject(projectType)) return ["git", ...(usesPackageManager && packageManager ? [packageManager] : [])];
|
|
33
35
|
if (isSwiftProject(projectType)) return ["git", "swift", ...(usesPackageManager && packageManager ? [packageManager] : [])];
|
|
34
36
|
return ["git", packageManager, "cargo", "rustc"];
|
|
35
37
|
}
|