@rightkit/release 0.2.173 → 0.3.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 +188 -5
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/darwin/rightkit-swap.sign.json +1 -1
- package/cli/right-release.mjs +34 -6
- package/command-spec.mjs +80 -0
- package/deps-cargo.mjs +55 -0
- package/deps.mjs +7 -0
- package/development-signing.mjs +135 -4
- package/development.mjs +12 -11
- package/dist-scan.mjs +102 -0
- package/dmg-build.mjs +53 -4
- package/icon-pack.mjs +117 -0
- package/in-place-install.mjs +32 -7
- package/init-command.mjs +116 -0
- package/install-app-windows.mjs +108 -17
- package/install-app.mjs +68 -11
- package/install-tauri-devid.mjs +106 -47
- package/local-publish.mjs +317 -29
- package/mac-bundle-signing.mjs +143 -1
- package/mac-signing-matrix.mjs +73 -6
- package/macho-approval-hash.mjs +181 -0
- package/macos-signing-session.mjs +12 -2
- package/managed-cargo.mjs +49 -1
- package/media-sidecar.mjs +20 -4
- package/mirror-root-artifact.mjs +69 -21
- package/npm-license-notices.mjs +159 -0
- package/ownership-dependency-allowlist.json +1 -0
- package/ownership.mjs +37 -4
- package/package-recipe-targets.mjs +180 -16
- package/package.json +1 -1
- package/pe-content.mjs +133 -0
- package/portable-development.mjs +9 -9
- package/publication-gate.mjs +70 -3
- package/publish-cargo.mjs +26 -7
- package/qa-build-features.mjs +7 -1
- package/recipe-build.mjs +346 -0
- package/release-dependencies-command.mjs +45 -0
- package/release-dependencies.mjs +60 -0
- package/release-switch-scan.mjs +17 -7
- package/release.mjs +98 -12
- package/rightkit-ownership.json +46 -2
- package/rightkit-signing-agent.mjs +10 -4
- package/rightkit-versions.json +11 -6
- package/runtime-budgets.mjs +28 -12
- package/stage-binary.mjs +1 -1
- package/stage-sidecar-command.mjs +36 -4
- package/stage-sidecar.mjs +926 -74
- package/test-command.mjs +190 -0
- package/toolchain-swift.mjs +239 -0
- package/web-build.mjs +67 -0
package/mirror-root-artifact.mjs
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
// Two forms:
|
|
3
|
+
// right-release mirror-root-artifact --file <artifact> --package-root <dir> [--name <filename>]
|
|
4
|
+
// right-release mirror-root-artifact --config <right-release.config.mjs> --platform mac|win [--package-root <dir>]
|
|
5
|
+
// The config form mirrors every entry of `targets.<platform>.mirror`, a list of `"<app-relative file>"` or
|
|
6
|
+
// `{ file, name?, pruneSuffix? }`. `pruneSuffix` (for example "-setup.exe") then removes the other files with that suffix
|
|
7
|
+
// from the managed directory, so it holds exactly the current artifact. Run it as `targets.<platform>.postSign`.
|
|
8
|
+
// Output goes only to RIGHT_RELEASE_ARTIFACT_DIR (outside the repository); it never writes into the repository root.
|
|
9
|
+
import { copyFileSync, lstatSync, mkdirSync, readdirSync, realpathSync, renameSync, rmSync, statSync } from "node:fs";
|
|
3
10
|
import { execFileSync } from "./background-process.mjs";
|
|
4
11
|
import path from "node:path";
|
|
12
|
+
import { pathToFileURL } from "node:url";
|
|
5
13
|
|
|
6
14
|
const args = parseArgs(process.argv.slice(2));
|
|
7
|
-
|
|
8
|
-
|
|
15
|
+
const usage = "Usage: right-release mirror-root-artifact (--file <artifact> --package-root <dir> [--name <filename>] | --config <file> --platform mac|win [--package-root <dir>])";
|
|
16
|
+
if (!args.config && (!args.file || !args.packageRoot)) {
|
|
17
|
+
console.error(usage);
|
|
18
|
+
process.exit(2);
|
|
19
|
+
}
|
|
20
|
+
if (args.config && (!args.platform || args.file)) {
|
|
21
|
+
console.error(usage);
|
|
9
22
|
process.exit(2);
|
|
10
23
|
}
|
|
11
24
|
if (!process.env.RIGHT_RELEASE_ARTIFACT_DIR) {
|
|
@@ -13,33 +26,68 @@ if (!process.env.RIGHT_RELEASE_ARTIFACT_DIR) {
|
|
|
13
26
|
process.exit(2);
|
|
14
27
|
}
|
|
15
28
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
29
|
+
let entries;
|
|
30
|
+
let rootArg = args.packageRoot;
|
|
31
|
+
if (args.config) {
|
|
32
|
+
const configPath = path.resolve(args.config);
|
|
33
|
+
const loaded = await import(pathToFileURL(configPath).href);
|
|
34
|
+
const config = loaded.default ?? loaded.config ?? loaded;
|
|
35
|
+
const declared = config?.targets?.[args.platform]?.mirror;
|
|
36
|
+
if (!Array.isArray(declared) || declared.length === 0) {
|
|
37
|
+
console.error(`right-release mirror-root-artifact: targets.${args.platform}.mirror declares no files in ${args.config}`);
|
|
38
|
+
process.exit(2);
|
|
39
|
+
}
|
|
40
|
+
entries = declared.map((item) => (typeof item === "string" ? { file: item } : item));
|
|
41
|
+
for (const entry of entries) {
|
|
42
|
+
const unknown = entry && typeof entry === "object" ? Object.keys(entry).filter((key) => !["file", "name", "pruneSuffix"].includes(key)) : ["(not an object)"];
|
|
43
|
+
if (typeof entry?.file !== "string" || !entry.file.trim() || unknown.length || path.isAbsolute(entry.file) || entry.file.replaceAll("\\", "/").split("/").includes("..")) {
|
|
44
|
+
console.error(`right-release mirror-root-artifact: each mirror entry is an app-relative file or { file, name?, pruneSuffix? }: ${JSON.stringify(entry)}`);
|
|
45
|
+
process.exit(2);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
rootArg ??= path.dirname(configPath);
|
|
49
|
+
} else {
|
|
50
|
+
entries = [{ file: args.file, name: args.name }];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const packageRoot = realpathSync(path.resolve(rootArg));
|
|
19
54
|
const managedOutput = path.resolve(process.env.RIGHT_RELEASE_ARTIFACT_DIR);
|
|
20
55
|
const repoRoot = path.resolve(git(packageRoot, "rev-parse", "--show-toplevel"));
|
|
21
56
|
if (inside(managedOutput, repoRoot)) {
|
|
22
57
|
console.error(`right-release mirror-root-artifact: managed output must be outside repository: ${managedOutput}`);
|
|
23
58
|
process.exit(2);
|
|
24
59
|
}
|
|
25
|
-
|
|
26
|
-
console.error(`right-release mirror-root-artifact: unsafe artifact name: ${name}`);
|
|
27
|
-
process.exit(2);
|
|
28
|
-
}
|
|
29
|
-
assertSafePath(source);
|
|
30
|
-
if (!statSync(source).isFile()) {
|
|
31
|
-
console.error(`right-release mirror-root-artifact: source is not a regular file: ${source}`);
|
|
32
|
-
process.exit(2);
|
|
33
|
-
}
|
|
34
|
-
const destinations = new Set([path.join(managedOutput, name)]);
|
|
60
|
+
const safeName = (name) => name === path.basename(name) && /^[A-Za-z0-9][A-Za-z0-9._+-]{0,159}$/.test(name);
|
|
35
61
|
|
|
36
|
-
for (const
|
|
37
|
-
|
|
38
|
-
|
|
62
|
+
for (const entry of entries) {
|
|
63
|
+
const source = path.resolve(packageRoot, entry.file);
|
|
64
|
+
const name = entry.name || path.basename(source);
|
|
65
|
+
if (!safeName(name)) {
|
|
66
|
+
console.error(`right-release mirror-root-artifact: unsafe artifact name: ${name}`);
|
|
67
|
+
process.exit(2);
|
|
68
|
+
}
|
|
69
|
+
if (entry.pruneSuffix !== undefined && (typeof entry.pruneSuffix !== "string" || !/^[A-Za-z0-9._+-]{1,40}$/.test(entry.pruneSuffix) || !name.endsWith(entry.pruneSuffix))) {
|
|
70
|
+
console.error(`right-release mirror-root-artifact: pruneSuffix must be a plain suffix of the artifact name: ${entry.pruneSuffix}`);
|
|
71
|
+
process.exit(2);
|
|
72
|
+
}
|
|
73
|
+
assertSafePath(source);
|
|
74
|
+
if (!statSync(source).isFile()) {
|
|
75
|
+
console.error(`right-release mirror-root-artifact: source is not a regular file: ${source}`);
|
|
76
|
+
process.exit(2);
|
|
77
|
+
}
|
|
78
|
+
const destination = path.join(managedOutput, name);
|
|
79
|
+
if (path.resolve(destination) !== source) atomicCopy(source, destination);
|
|
80
|
+
if (entry.pruneSuffix) {
|
|
81
|
+
for (const stale of readdirSync(managedOutput)) {
|
|
82
|
+
if (stale !== name && stale.endsWith(entry.pruneSuffix) && safeName(stale)) {
|
|
83
|
+
assertSafePath(path.join(managedOutput, stale));
|
|
84
|
+
rmSync(path.join(managedOutput, stale), { force: true });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
console.log(`right-release: managed artifact -> ${destination}`);
|
|
39
89
|
}
|
|
40
90
|
|
|
41
|
-
console.log(`right-release: managed artifact -> ${[...destinations][0]}`);
|
|
42
|
-
|
|
43
91
|
function git(cwd, ...commandArgs) {
|
|
44
92
|
return execFileSync("git", commandArgs, { cwd, encoding: "utf8" }).trim();
|
|
45
93
|
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `right-release license-notices npm [--config <file>] [--output <file>] [--product-name <name>] [--check]`: writes the
|
|
3
|
+
// third-party npm license appendix (one HTML file) for an app's production dependency closure, from
|
|
4
|
+
// `pnpm licenses list --prod --json`, the installed packages' own LICENSE / COPYING / NOTICE files, and the lockfile hash.
|
|
5
|
+
//
|
|
6
|
+
// legal: { npmNotices: { output: "legal/THIRD_PARTY_NPM_LICENSES.html", productName: "ViewRight", exclude: ["@rightkit/"] } }
|
|
7
|
+
//
|
|
8
|
+
// `output` and `productName` may also come from `--output` / `--product-name`. `exclude` is a list of package-name prefixes left
|
|
9
|
+
// out (default `["@rightkit/"]`). A package that ships no license file gets the standard published text for its declared SPDX
|
|
10
|
+
// license (MIT or ISC), labelled as a fallback; any other license without a file is an error, never invented text.
|
|
11
|
+
// The page records the app version, the sha256 of `pnpm-lock.yaml` and `generated-by=right-release license-notices npm`, so the
|
|
12
|
+
// same inputs always produce the same bytes. `--check` writes nothing and exits 1 when the file on disk differs.
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
14
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { pathToFileURL } from "node:url";
|
|
17
|
+
import { spawnSync } from "./background-process.mjs";
|
|
18
|
+
import { CommandSpecError, loadAppConfig, relativeInside } from "./command-spec.mjs";
|
|
19
|
+
|
|
20
|
+
const fail = (message) => { throw new CommandSpecError(message); };
|
|
21
|
+
|
|
22
|
+
// Generic published SPDX text, used ONLY when an installed package ships no license file of its own.
|
|
23
|
+
export const SPDX_FALLBACK_TEXT = {
|
|
24
|
+
MIT: `Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
25
|
+
|
|
26
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
27
|
+
|
|
28
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.`,
|
|
29
|
+
ISC: `Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
|
|
30
|
+
|
|
31
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.`,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const escapeHtml = (value) => String(value)
|
|
35
|
+
.replaceAll("&", "&")
|
|
36
|
+
.replaceAll("<", "<")
|
|
37
|
+
.replaceAll(">", ">")
|
|
38
|
+
.replaceAll('"', """);
|
|
39
|
+
|
|
40
|
+
/** Flatten `pnpm licenses list --json` (license -> entries) into a sorted list, minus excluded name prefixes. */
|
|
41
|
+
export function collectNpmPackages(groups, exclude = ["@rightkit/"]) {
|
|
42
|
+
return Object.entries(groups)
|
|
43
|
+
.flatMap(([license, entries]) => entries.map((entry) => ({ ...entry, license })))
|
|
44
|
+
.filter((entry) => !exclude.some((prefix) => entry.name.startsWith(prefix)))
|
|
45
|
+
.sort((a, b) => `${a.name}@${a.versions.join(",")}`.localeCompare(`${b.name}@${b.versions.join(",")}`));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Render the appendix. `readNames(dir)` lists a package directory and `readText(file)` reads one file (both injectable).
|
|
50
|
+
* Returns `{ html, packageCount, fallbackCount }`.
|
|
51
|
+
*/
|
|
52
|
+
export function renderNpmNotices({ productName, version, lockHash, packages, readNames = (dir) => readdirSync(dir), readText = (file) => readFileSync(file, "utf8") }) {
|
|
53
|
+
let fallbackCount = 0;
|
|
54
|
+
const sections = packages.map((entry) => {
|
|
55
|
+
const packageRoot = entry.paths[0];
|
|
56
|
+
const files = readNames(packageRoot).filter((name) => /^(?:license|copying|notice)(?:[._-]|$)/i.test(name)).sort();
|
|
57
|
+
let texts;
|
|
58
|
+
if (files.length) {
|
|
59
|
+
texts = files.map((name) => `<h3>${escapeHtml(name)}</h3><pre>${escapeHtml(readText(path.join(packageRoot, name)))}</pre>`).join("\n");
|
|
60
|
+
} else {
|
|
61
|
+
const fallback = SPDX_FALLBACK_TEXT[entry.license];
|
|
62
|
+
if (!fallback) {
|
|
63
|
+
fail(`${entry.name} ${entry.versions.join(", ")} has no packaged license/notice file and no SPDX fallback text for declared license "${entry.license}"; bundle the file or extend SPDX_FALLBACK_TEXT`);
|
|
64
|
+
}
|
|
65
|
+
fallbackCount += 1;
|
|
66
|
+
texts = `<h3>No packaged license file found</h3><p><em>This package's installed npm distribution does not include a LICENSE/COPYING/NOTICE file. The text below is the standard published license text for its declared SPDX license (${escapeHtml(entry.license)}), not package-specific content.</em></p><pre>${escapeHtml(fallback)}</pre>`;
|
|
67
|
+
}
|
|
68
|
+
return `<section><h2>${escapeHtml(entry.name)} ${escapeHtml(entry.versions.join(", "))}</h2><p>Declared license: ${escapeHtml(entry.license)}</p>${texts}</section>`;
|
|
69
|
+
});
|
|
70
|
+
const html = `<!doctype html>
|
|
71
|
+
<html lang="en"><head><meta charset="utf-8"><title>${escapeHtml(productName)} npm third-party licenses</title></head><body>
|
|
72
|
+
<h1>${escapeHtml(productName)} npm third-party licenses</h1>
|
|
73
|
+
<p>Build provenance: ${escapeHtml(productName)} ${escapeHtml(version)}; inputs: pnpm-lock.yaml sha256=${lockHash}; generated-by=right-release license-notices npm</p>
|
|
74
|
+
${sections.join("\n")}
|
|
75
|
+
</body></html>\n`;
|
|
76
|
+
return { html, packageCount: packages.length, fallbackCount };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Resolve options from config + flags. */
|
|
80
|
+
export function resolveNoticeOptions(config, flags = {}) {
|
|
81
|
+
const declared = config?.legal?.npmNotices ?? {};
|
|
82
|
+
if (typeof declared !== "object" || declared === null || Array.isArray(declared)) fail("legal.npmNotices must be an object");
|
|
83
|
+
const unknown = Object.keys(declared).filter((key) => !["output", "productName", "exclude"].includes(key));
|
|
84
|
+
if (unknown.length) fail(`legal.npmNotices has unknown key(s): ${unknown.join(", ")} (allowed: output, productName, exclude)`);
|
|
85
|
+
const output = flags.output ?? declared.output;
|
|
86
|
+
const productName = flags.productName ?? declared.productName;
|
|
87
|
+
const exclude = declared.exclude ?? ["@rightkit/"];
|
|
88
|
+
if (output === undefined) fail("no output file: set legal.npmNotices.output or pass --output");
|
|
89
|
+
relativeInside(output, "legal.npmNotices.output");
|
|
90
|
+
if (typeof productName !== "string" || !productName.trim()) fail("no product name: set legal.npmNotices.productName or pass --product-name");
|
|
91
|
+
if (!Array.isArray(exclude) || exclude.some((prefix) => typeof prefix !== "string")) fail("legal.npmNotices.exclude must be a list of name prefixes");
|
|
92
|
+
if ((config?.packageManager ?? "pnpm") !== "pnpm") fail(`license-notices npm supports pnpm apps only (config.packageManager is ${config.packageManager})`);
|
|
93
|
+
return { output, productName, exclude };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Generate (or check) the appendix for the app at `appRoot`. `spawnSyncFn` is injectable. */
|
|
97
|
+
export function generateNpmNotices({ appRoot, config, flags = {}, check = false, spawnSyncFn = spawnSync }) {
|
|
98
|
+
const { output, productName, exclude } = resolveNoticeOptions(config, flags);
|
|
99
|
+
const version = JSON.parse(readFileSync(path.join(appRoot, "package.json"), "utf8")).version;
|
|
100
|
+
const result = spawnSyncFn("pnpm", ["licenses", "list", "--prod", "--json"], {
|
|
101
|
+
cwd: appRoot,
|
|
102
|
+
encoding: "utf8",
|
|
103
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
104
|
+
shell: process.platform === "win32",
|
|
105
|
+
});
|
|
106
|
+
if (result.error) throw result.error;
|
|
107
|
+
if (result.status !== 0) fail(result.stderr || "pnpm licenses list failed");
|
|
108
|
+
const packages = collectNpmPackages(JSON.parse(result.stdout), exclude);
|
|
109
|
+
const lockHash = createHash("sha256").update(readFileSync(path.join(appRoot, "pnpm-lock.yaml"))).digest("hex");
|
|
110
|
+
const { html, packageCount, fallbackCount } = renderNpmNotices({ productName, version, lockHash, packages });
|
|
111
|
+
const target = path.join(appRoot, output);
|
|
112
|
+
if (check) {
|
|
113
|
+
const current = existsSync(target) ? readFileSync(target, "utf8") : null;
|
|
114
|
+
return { target, packageCount, fallbackCount, upToDate: current === html };
|
|
115
|
+
}
|
|
116
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
117
|
+
writeFileSync(target, html);
|
|
118
|
+
return { target, packageCount, fallbackCount, upToDate: true };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function parseNoticeArgs(argv) {
|
|
122
|
+
const opts = { config: "right-release.config.mjs", output: undefined, productName: undefined, check: false };
|
|
123
|
+
const rest = argv[0] === "npm" ? argv.slice(1) : argv;
|
|
124
|
+
if (argv[0] !== "npm") throw new Error("expected: license-notices npm");
|
|
125
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
126
|
+
const arg = rest[i];
|
|
127
|
+
if (arg === "--config" || arg === "--output" || arg === "--product-name") {
|
|
128
|
+
const value = rest[i + 1];
|
|
129
|
+
if (!value || value.startsWith("--")) throw new Error(`${arg} needs a value`);
|
|
130
|
+
opts[arg === "--product-name" ? "productName" : arg.slice(2)] = value;
|
|
131
|
+
i += 1;
|
|
132
|
+
} else if (arg === "--check") opts.check = true;
|
|
133
|
+
else throw new Error(`unknown license-notices argument: ${arg}`);
|
|
134
|
+
}
|
|
135
|
+
return opts;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
139
|
+
const opts = parseNoticeArgs(argv);
|
|
140
|
+
const { config, appRoot } = await loadAppConfig(opts.config);
|
|
141
|
+
const outcome = generateNpmNotices({ appRoot, config, flags: { output: opts.output, productName: opts.productName }, check: opts.check });
|
|
142
|
+
if (opts.check) {
|
|
143
|
+
if (!outcome.upToDate) {
|
|
144
|
+
console.error(`right-release license-notices: ${outcome.target} is out of date; run right-release license-notices npm`);
|
|
145
|
+
return 1;
|
|
146
|
+
}
|
|
147
|
+
console.log(`${outcome.target} is up to date (${outcome.packageCount} package notices)`);
|
|
148
|
+
return 0;
|
|
149
|
+
}
|
|
150
|
+
console.log(`wrote ${outcome.packageCount} package notices (${outcome.fallbackCount} used an SPDX fallback text)`);
|
|
151
|
+
return 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
|
155
|
+
main().then((code) => { process.exitCode = code; }, (error) => {
|
|
156
|
+
console.error(`right-release license-notices: ${error.code ? `[${error.code}] ` : ""}${error.message}`);
|
|
157
|
+
process.exitCode = 1;
|
|
158
|
+
});
|
|
159
|
+
}
|
package/ownership.mjs
CHANGED
|
@@ -156,7 +156,31 @@ function resolveLocalPath(root, sourceFile, pathValue, coverage, display, invent
|
|
|
156
156
|
} catch { coverage.push({ path: display, status: "unscanned", reason: `${reasonPrefix}-path-unresolved${detail ? `:${detail}` : ""}` }); }
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
|
|
159
|
+
// A Git dependency pinned by a full 40-hex `rev` is immutable. When its URL is third-party it is equivalent to a registry
|
|
160
|
+
// dependency and is not a gap (Orthic-Labs/rightkit#677). A first-party URL (Orthic-Labs, bogusyogi, or an org listed in
|
|
161
|
+
// ownership-repositories.json) stays an unscanned fatal gap because first-party code must be scanned. A branch, a tag, a
|
|
162
|
+
// short rev or no rev is always an unscanned coverage gap.
|
|
163
|
+
const FULL_GIT_REV = /^[0-9a-f]{40}$/i;
|
|
164
|
+
export const PINNED_GIT_DEPENDENCY = "pinned-git-dependency";
|
|
165
|
+
const FIRST_PARTY_GIT_ORGS = new Set(["github.com/orthic-labs", "github.com/bogusyogi"]);
|
|
166
|
+
const gitHostOrg = (url) => {
|
|
167
|
+
const text = String(url).trim();
|
|
168
|
+
const match = /^(?:[a-z][a-z0-9+.-]*:\/\/)?(?:[^@/\s]+@)?([^/:\s]+)[/:]+([^/\s]+)/i.exec(text);
|
|
169
|
+
return match ? `${match[1]}/${match[2]}`.toLowerCase() : null;
|
|
170
|
+
};
|
|
171
|
+
let configuredFirstParty;
|
|
172
|
+
function firstPartyGitOrgs() {
|
|
173
|
+
if (configuredFirstParty) return configuredFirstParty;
|
|
174
|
+
const orgs = new Set(FIRST_PARTY_GIT_ORGS);
|
|
175
|
+
try {
|
|
176
|
+
const config = JSON.parse(readFileSync(new URL("./ownership-repositories.json", import.meta.url), "utf8"));
|
|
177
|
+
for (const repo of config.repositories ?? []) { const key = gitHostOrg(repo.repositoryIdentity ?? ""); if (key) orgs.add(key); }
|
|
178
|
+
} catch { /* the built-in first-party orgs still apply */ }
|
|
179
|
+
return (configuredFirstParty = orgs);
|
|
180
|
+
}
|
|
181
|
+
export const isFirstPartyGitUrl = (url) => { const key = gitHostOrg(url); return key === null || firstPartyGitOrgs().has(key); };
|
|
182
|
+
|
|
183
|
+
function parseCargo(parsed, file, out, workspace, root, coverage, inventory, covered = []) {
|
|
160
184
|
const pathSpec = (pathValue, sourceFile, display) => {
|
|
161
185
|
resolveLocalPath(root, sourceFile, pathValue, coverage, display, inventory);
|
|
162
186
|
};
|
|
@@ -176,7 +200,15 @@ function parseCargo(parsed, file, out, workspace, root, coverage, inventory) {
|
|
|
176
200
|
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
201
|
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
202
|
if (pathValue) pathSpec(pathValue, spec.path ? file : path.posix.join(workspace.dir ?? ".", "Cargo.toml"), file);
|
|
179
|
-
if (git)
|
|
203
|
+
if (git) {
|
|
204
|
+
const rev = spec.rev ?? inherited.rev;
|
|
205
|
+
const moving = [spec.branch, inherited.branch, spec.tag, inherited.tag].some((value) => value !== undefined);
|
|
206
|
+
if (typeof git === "string" && typeof rev === "string" && FULL_GIT_REV.test(rev) && !moving && !isFirstPartyGitUrl(git)) {
|
|
207
|
+
// Immutable third-party source, not a gap: kept out of `coverage` (every coverage row blocks the gate) and reported on
|
|
208
|
+
// `deps.covered`. `scanned: false` records that the source is pinned, not examined.
|
|
209
|
+
covered.push({ path: file, status: PINNED_GIT_DEPENDENCY, reason: `git-dependency-source:${name}`, fingerprint: `${git}@${rev.toLowerCase()}`, scanned: false });
|
|
210
|
+
} else coverage.push({ path: file, status: "unscanned", reason: `git-dependency-source:${name}` });
|
|
211
|
+
}
|
|
180
212
|
}
|
|
181
213
|
for (const [target, value] of Object.entries(table.target ?? {})) addTable(value, `${prefix}target.'${target}'.`);
|
|
182
214
|
};
|
|
@@ -375,7 +407,7 @@ function resolveDependencyVersions(appRoot, inventory, deps, coverage, contentDi
|
|
|
375
407
|
}
|
|
376
408
|
|
|
377
409
|
function dependenciesFromInventory(appRoot, inventory) {
|
|
378
|
-
const deps = [], coverage = [], contentDigests = [];
|
|
410
|
+
const deps = [], coverage = [], contentDigests = [], covered = [];
|
|
379
411
|
// Workspace inheritance is loaded before member manifests.
|
|
380
412
|
const manifestNames = new Set(["Cargo.toml", "package.json", "Package.swift"]);
|
|
381
413
|
const manifests = inventory.files.filter((x) => manifestNames.has(path.posix.basename(x.rel)));
|
|
@@ -419,7 +451,7 @@ function dependenciesFromInventory(appRoot, inventory) {
|
|
|
419
451
|
if (base === "Cargo.toml") {
|
|
420
452
|
const dir = path.posix.dirname(rel);
|
|
421
453
|
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); }
|
|
454
|
+
try { parseCargo(parseToml(text), rel, deps, workspace, appRoot, coverage, inventory, covered); }
|
|
423
455
|
catch { coverage.push({ path: rel, status: "unscanned", reason: "invalid-cargo-toml" }); }
|
|
424
456
|
}
|
|
425
457
|
else if (base === "package.json") {
|
|
@@ -453,6 +485,7 @@ function dependenciesFromInventory(appRoot, inventory) {
|
|
|
453
485
|
resolveDependencyVersions(appRoot, inventory, deps, coverage, contentDigests);
|
|
454
486
|
Object.defineProperty(deps, "coverage", { value: coverage, enumerable: false });
|
|
455
487
|
Object.defineProperty(deps, "contentDigests", { value: contentDigests, enumerable: false });
|
|
488
|
+
Object.defineProperty(deps, "covered", { value: covered, enumerable: false });
|
|
456
489
|
Object.defineProperty(deps, "manifests", { value: files, enumerable: false });
|
|
457
490
|
return deps;
|
|
458
491
|
}
|
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
// marker patch before signing, raw-EXE and installer Authenticode, the NSIS embedding verification, Developer ID
|
|
5
5
|
// signing of the .app and its nested code, app notarization before packing, the signed DMG and its notarization.
|
|
6
6
|
// The standalone recipes (win-package-recipe.mjs, mac-package-recipe.mjs) hold the same steps for direct callers.
|
|
7
|
+
import { existsSync } from "node:fs";
|
|
7
8
|
import path from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { resolveTargetRoot as cargoResolveTargetRoot } from "./cargo-target.mjs";
|
|
8
11
|
import { validateWinPackage, WinPackageConfigError } from "./win-package-recipe.mjs";
|
|
9
12
|
|
|
10
13
|
export class PackageRecipeTargetError extends Error {
|
|
@@ -21,40 +24,166 @@ const withoutRecipe = (target) => {
|
|
|
21
24
|
return rest;
|
|
22
25
|
};
|
|
23
26
|
const QUIET_TAURI_ENV = { TAURI_SIGNING_PRIVATE_KEY: "", TAURI_SIGNING_PRIVATE_KEY_PASSWORD: "" };
|
|
27
|
+
const RECIPE_BUILD = fileURLToPath(new URL("./recipe-build.mjs", import.meta.url));
|
|
28
|
+
const CREATE_MAC_UPDATER = fileURLToPath(new URL("./create-mac-updater.mjs", import.meta.url));
|
|
29
|
+
const PROFILE = /^[a-z][a-z0-9_-]*$/;
|
|
30
|
+
|
|
31
|
+
function relativeInside(value, label) {
|
|
32
|
+
if (typeof value !== "string" || !value.trim()) fail(`${label} must be a non-empty relative path`);
|
|
33
|
+
const normalized = value.replaceAll("\\", "/");
|
|
34
|
+
if (path.isAbsolute(value) || /^[A-Za-z]:/.test(value) || normalized.split("/").includes("..")) fail(`${label} must be relative and stay inside the app: ${value}`);
|
|
35
|
+
return normalized;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** `prepare` / `postBuild`: a list of { cmd, args?, cwd? } argv commands (no shell). */
|
|
39
|
+
function commandList(value, label) {
|
|
40
|
+
if (!Array.isArray(value)) fail(`${label} must be an array of { cmd, args?, cwd? }`);
|
|
41
|
+
return value.map((item, index) => {
|
|
42
|
+
const where = `${label}[${index}]`;
|
|
43
|
+
if (!isObject(item)) fail(`${where} must be an object`);
|
|
44
|
+
const unknown = Object.keys(item).filter((key) => !["cmd", "args", "cwd"].includes(key));
|
|
45
|
+
if (unknown.length) fail(`${where} has unknown key(s): ${unknown.join(", ")} (allowed: cmd, args, cwd)`);
|
|
46
|
+
if (typeof item.cmd !== "string" || !item.cmd.trim()) fail(`${where}.cmd must be a non-empty string`);
|
|
47
|
+
if (item.args !== undefined && (!Array.isArray(item.args) || item.args.some((arg) => typeof arg !== "string"))) fail(`${where}.args must be a list of strings`);
|
|
48
|
+
return { cmd: item.cmd, args: [...(item.args ?? [])], ...(item.cwd === undefined ? {} : { cwd: relativeInside(item.cwd, `${where}.cwd`) }) };
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The keys shared by winPackage and macPackage: profile, tauriConfig, prepare (and postBuild where allowed). */
|
|
53
|
+
function buildExtras(spec, label, { postBuild }) {
|
|
54
|
+
if (spec.profile !== undefined && (typeof spec.profile !== "string" || !PROFILE.test(spec.profile))) fail(`${label}.profile must be a Cargo profile name`);
|
|
55
|
+
const tauriConfig = spec.tauriConfig === undefined ? undefined : relativeInside(spec.tauriConfig, `${label}.tauriConfig`);
|
|
56
|
+
if (tauriConfig && !/\.json$/i.test(tauriConfig)) fail(`${label}.tauriConfig must be a .json file`);
|
|
57
|
+
return {
|
|
58
|
+
profile: spec.profile,
|
|
59
|
+
tauriConfig,
|
|
60
|
+
prepare: spec.prepare === undefined ? [] : commandList(spec.prepare, `${label}.prepare`),
|
|
61
|
+
postBuild: !postBuild || spec.postBuild === undefined ? [] : commandList(spec.postBuild, `${label}.postBuild`),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Tauri v2 passes Cargo flags after `--`; the profile also decides where Tauri looks for the built binary. */
|
|
66
|
+
const profileArgs = (extras) => (extras.profile ? ["--", "--profile", extras.profile] : []);
|
|
67
|
+
/** The build command also takes `--jobs N` (Cargo parallelism), after the same `--`. */
|
|
68
|
+
const buildCargoArgs = (extras, jobs) => {
|
|
69
|
+
const flags = [...(extras.profile ? ["--profile", extras.profile] : []), ...(jobs ? ["--jobs", String(jobs)] : [])];
|
|
70
|
+
return flags.length ? ["--", ...flags] : [];
|
|
71
|
+
};
|
|
72
|
+
const configArgs = (extras, appRoot, cwd) => (extras.tauriConfig ? ["--config", path.relative(path.resolve(appRoot, cwd), path.resolve(appRoot, extras.tauriConfig)).split(path.sep).join("/")] : []);
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* release.mjs runs ONE prePackage command: node recipe-build.mjs runs the built-in cleanup, prepare, the Tauri build,
|
|
76
|
+
* the artifact stability poll and postBuild in order. `cleanup` and `stability` are recipe-build plan sections.
|
|
77
|
+
*/
|
|
78
|
+
function recipeBuildCommand({ extras, build, env, cleanup, stability, timeoutMs }) {
|
|
79
|
+
const steps = [
|
|
80
|
+
...extras.prepare.map((step) => ({ phase: "prepare", ...step })),
|
|
81
|
+
{ phase: "build", cmd: build.cmd, args: build.args, cwd: build.cwd },
|
|
82
|
+
...extras.postBuild.map((step) => ({ phase: "postBuild", ...step })),
|
|
83
|
+
];
|
|
84
|
+
const plan = { version: 1, steps, ...(cleanup ? { cleanup } : {}), ...(stability ? { stability } : {}) };
|
|
85
|
+
return { cmd: process.execPath, args: [RECIPE_BUILD, "--plan", JSON.stringify(plan)], env, ...(timeoutMs ? { timeoutMs } : {}) };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const DEFAULT_WIN_TIMEOUT_MS = 20 * 60 * 1000;
|
|
89
|
+
const positiveInt = (value) => Number.isInteger(value) && value > 0;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Managed Cargo target roots. Artifact paths in a target are declared (and default) under `<cargo dir>/target/...`.
|
|
93
|
+
* With `cargoTargetRoot` (true: <tauriCwd>/src-tauri/Cargo.toml; or an app-relative Cargo.toml path) or CARGO_TARGET_DIR set
|
|
94
|
+
* (default manifest), `cargo metadata`'s target_directory is resolved and every declared path under the default
|
|
95
|
+
* `<cargo dir>/target/` is moved to the real directory. release.mjs needs app-relative paths with no `..`, so a target
|
|
96
|
+
* directory outside the app fails closed (use RIGHT_RELEASE_ARTIFACT_DIR for copies, see the docs).
|
|
97
|
+
*/
|
|
98
|
+
function targetRootMapper({ spec, appRoot, cwd, env, resolveTargetRoot, label }) {
|
|
99
|
+
const explicit = spec.cargoTargetRoot;
|
|
100
|
+
if (explicit !== undefined && explicit !== true && explicit !== false && typeof explicit !== "string") fail(`${label}.cargoTargetRoot must be true or an app-relative Cargo.toml path`);
|
|
101
|
+
const envDir = typeof env.CARGO_TARGET_DIR === "string" && env.CARGO_TARGET_DIR.trim() !== "";
|
|
102
|
+
if (explicit === false || (!explicit && !envDir)) return (declared) => declared;
|
|
103
|
+
const manifestRel = typeof explicit === "string" ? relativeInside(explicit, `${label}.cargoTargetRoot`) : path.posix.join(cwd === "." ? "" : cwd, "src-tauri", "Cargo.toml");
|
|
104
|
+
const manifest = path.resolve(appRoot, manifestRel);
|
|
105
|
+
if (typeof explicit !== "string" && explicit !== true && !existsSync(manifest)) return (declared) => declared;
|
|
106
|
+
let targetDir;
|
|
107
|
+
try { targetDir = resolveTargetRoot(manifest); } catch (error) { fail(`${label}: cannot resolve the Cargo target directory (${error.message})`); }
|
|
108
|
+
const prefix = path.posix.join(path.posix.dirname(manifestRel).replace(/^\.$/, ""), "target");
|
|
109
|
+
return (declared) => {
|
|
110
|
+
const normalized = String(declared).replaceAll("\\", "/");
|
|
111
|
+
if (!normalized.startsWith(`${prefix}/`)) return declared;
|
|
112
|
+
const moved = path.resolve(targetDir, normalized.slice(prefix.length + 1));
|
|
113
|
+
const relative = path.relative(appRoot, moved).split(path.sep).join("/");
|
|
114
|
+
if (relative === "" || relative.startsWith("..") || path.isAbsolute(relative) || /^[A-Za-z]:/.test(relative)) {
|
|
115
|
+
fail(`${label}: the Cargo target directory ${targetDir} is outside the app, so ${declared} cannot be addressed by an app-relative path; point CARGO_TARGET_DIR inside the app or declare the artifact paths yourself`);
|
|
116
|
+
}
|
|
117
|
+
return relative;
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function winPrePackage({ extras, cwd, appRoot, triple, jobs, timeoutMs, cleanup, stability }) {
|
|
122
|
+
const args = ["exec", "tauri", "build", "--no-bundle", "--target", triple, ...configArgs(extras, appRoot, cwd), ...buildCargoArgs(extras, jobs)];
|
|
123
|
+
return recipeBuildCommand({ extras, build: { cmd: "pnpm", args, cwd }, env: QUIET_TAURI_ENV, cleanup, stability, timeoutMs });
|
|
124
|
+
}
|
|
24
125
|
|
|
25
126
|
/** `targets.win.winPackage` → prePackage (`tauri build --no-bundle`) and package (`tauri bundle --bundles nsis`). */
|
|
26
|
-
export function expandWinPackageTarget(target, { appRoot, config = {} }) {
|
|
127
|
+
export function expandWinPackageTarget(target, { appRoot, config = {}, env = process.env, resolveTargetRoot = cargoResolveTargetRoot }) {
|
|
27
128
|
if (!target?.winPackage) return withoutRecipe(target);
|
|
28
129
|
if (target.prePackage !== undefined) fail("targets.win.winPackage replaces prePackage; remove the prePackage script");
|
|
29
130
|
if (target.winPackage.sidecars !== undefined) fail("targets.win.winPackage.sidecars is not supported in a right-release target: declare sidecars through externalBin or mediaSidecar, which release.mjs signs with the raw EXE");
|
|
131
|
+
if (!isObject(target.winPackage)) fail("targets.win.winPackage must be an object");
|
|
132
|
+
const { profile: _p, tauriConfig: _t, prepare: _pr, postBuild, jobs, timeoutMs, cleanStale, cargoTargetRoot: _c, ...coreSpec } = target.winPackage;
|
|
133
|
+
if (postBuild !== undefined) fail("targets.win.winPackage.postBuild is not supported: release.mjs signs the raw EXE between the build and the bundle; use prepare");
|
|
134
|
+
if (jobs !== undefined && !positiveInt(jobs)) fail("targets.win.winPackage.jobs must be a positive integer (Cargo --jobs)");
|
|
135
|
+
if (timeoutMs !== undefined && !positiveInt(timeoutMs)) fail("targets.win.winPackage.timeoutMs must be a positive integer (milliseconds)");
|
|
136
|
+
if (cleanStale !== undefined && typeof cleanStale !== "boolean") fail("targets.win.winPackage.cleanStale must be a boolean");
|
|
137
|
+
const extras = buildExtras(target.winPackage, "targets.win.winPackage", { postBuild: false });
|
|
30
138
|
let plan;
|
|
31
|
-
try { plan = validateWinPackage({ appRoot, config, target }); } catch (error) {
|
|
139
|
+
try { plan = validateWinPackage({ appRoot, config, target: { ...target, winPackage: coreSpec } }); } catch (error) {
|
|
32
140
|
if (error instanceof WinPackageConfigError) fail(`targets.win.winPackage: ${(error.issues ?? [error.message]).join("; ")}`);
|
|
33
141
|
throw error;
|
|
34
142
|
}
|
|
35
143
|
if (target.signingContract !== undefined && target.signingContract !== "windows-raw-exe-authenticode-before-nsis-v1") fail(`targets.win.winPackage builds an NSIS installer; signingContract must be windows-raw-exe-authenticode-before-nsis-v1, got ${target.signingContract}`);
|
|
36
144
|
const cwd = rel(plan.appRoot, plan.tauriCwd) || ".";
|
|
37
145
|
const override = { bundle: { createUpdaterArtifacts: false, windows: { nsis: { installMode: plan.perMachine ? "perMachine" : "currentUser" } } } };
|
|
146
|
+
const mapPath = targetRootMapper({ spec: target.winPackage, appRoot: plan.appRoot, cwd, env, resolveTargetRoot, label: "targets.win.winPackage" });
|
|
147
|
+
const rawExe = mapPath(rel(plan.appRoot, plan.rawExe));
|
|
148
|
+
const installerFile = mapPath(rel(plan.appRoot, plan.installerFile));
|
|
149
|
+
// Stale outputs are removed before the build, and the raw EXE is polled until it stops changing after it.
|
|
150
|
+
const cleanup = cleanStale === false ? undefined : { remove: [rawExe, installerFile] };
|
|
151
|
+
const timeout = timeoutMs ?? DEFAULT_WIN_TIMEOUT_MS;
|
|
38
152
|
// `recipe` is set only here: a hand-written one is dropped so it can never select a recipe path.
|
|
39
153
|
const { winPackage, recipe: _ignoredWin, ...rest } = target;
|
|
40
154
|
return {
|
|
41
155
|
...rest,
|
|
42
156
|
signingContract: "windows-raw-exe-authenticode-before-nsis-v1",
|
|
43
|
-
prePackage: {
|
|
44
|
-
package: { cmd: "pnpm", args: ["exec", "tauri", "bundle", "--bundles", "nsis", "--target", plan.triple, "--config", JSON.stringify(override)], cwd, env: QUIET_TAURI_ENV },
|
|
45
|
-
sign: { ...(target.sign ?? {}), prePackageFiles: [
|
|
157
|
+
prePackage: winPrePackage({ extras, cwd, appRoot: plan.appRoot, triple: plan.triple, jobs, timeoutMs: timeout, cleanup, stability: { files: [rawExe] } }),
|
|
158
|
+
package: { cmd: "pnpm", args: ["exec", "tauri", "bundle", "--bundles", "nsis", "--target", plan.triple, ...configArgs(extras, plan.appRoot, cwd), "--config", JSON.stringify(override), ...profileArgs(extras)], cwd, env: QUIET_TAURI_ENV, timeoutMs: timeout },
|
|
159
|
+
sign: { ...(target.sign ?? {}), prePackageFiles: [rawExe], files: [installerFile] },
|
|
46
160
|
recipe: { kind: "winPackage", productName: plan.productName, updater: plan.updater },
|
|
47
161
|
};
|
|
48
162
|
}
|
|
49
163
|
|
|
50
|
-
const MAC_KEYS = new Set(["universal", "tauriCwd", "features"]);
|
|
164
|
+
const MAC_KEYS = new Set(["universal", "triple", "tauriCwd", "features", "profile", "tauriConfig", "prepare", "postBuild", "cleanup", "cargoTargetRoot", "updater"]);
|
|
165
|
+
const MAC_TRIPLES = ["aarch64-apple-darwin", "x86_64-apple-darwin"];
|
|
166
|
+
const MAC_CLEANUP_KEYS = new Set(["staleApps", "staleDmgVolumes", "lsclean"]);
|
|
167
|
+
|
|
168
|
+
function macCleanupSpec(value) {
|
|
169
|
+
if (value === undefined) return {};
|
|
170
|
+
if (!isObject(value)) fail("targets.mac.macPackage.cleanup must be an object");
|
|
171
|
+
const unknown = Object.keys(value).filter((key) => !MAC_CLEANUP_KEYS.has(key));
|
|
172
|
+
if (unknown.length) fail(`targets.mac.macPackage.cleanup has unknown key(s): ${unknown.join(", ")} (allowed: ${[...MAC_CLEANUP_KEYS].join(", ")})`);
|
|
173
|
+
for (const key of MAC_CLEANUP_KEYS) if (value[key] !== undefined && typeof value[key] !== "boolean") fail(`targets.mac.macPackage.cleanup.${key} must be a boolean`);
|
|
174
|
+
return value;
|
|
175
|
+
}
|
|
51
176
|
|
|
52
177
|
/**
|
|
53
178
|
* `targets.mac.macPackage` → prePackage (`tauri build --bundles app`, universal by default), the .app as the
|
|
54
179
|
* Developer ID signing set, app notarization before packing, and right-release's own signed DMG from the stapled app.
|
|
55
180
|
* The app path and DMG path are `notarize.app` and `notarize.file`; DMG branding is `targets.mac.dmg`.
|
|
181
|
+
* Architecture: `triple` (an explicit arch triple) beats `universal`; `universal: false` means this host's arch.
|
|
182
|
+
* `cleanup` runs built-in steps in recipe-build before the build (stale DMG volumes, stale .app bundles, lsregister).
|
|
183
|
+
* `updater: true | "<app-relative .tar.gz>"` emits the Tauri updater tarball and its signature from the signed, stapled .app:
|
|
184
|
+
* it prepends a `create-mac-updater` command to `postSign` (default output `<App>.app.tar.gz` beside the app's working root).
|
|
56
185
|
*/
|
|
57
|
-
export function expandMacPackageTarget(target, { appRoot }) {
|
|
186
|
+
export function expandMacPackageTarget(target, { appRoot, env = process.env, resolveTargetRoot = cargoResolveTargetRoot }) {
|
|
58
187
|
if (!target?.macPackage) return withoutRecipe(target);
|
|
59
188
|
const spec = target.macPackage;
|
|
60
189
|
if (!isObject(spec)) fail("targets.mac.macPackage must be an object");
|
|
@@ -66,30 +195,65 @@ export function expandMacPackageTarget(target, { appRoot }) {
|
|
|
66
195
|
if (target.dmg === false) fail("targets.mac.macPackage always builds a DMG; remove dmg: false");
|
|
67
196
|
if (spec.universal !== undefined && typeof spec.universal !== "boolean") fail("targets.mac.macPackage.universal must be a boolean");
|
|
68
197
|
if (spec.features !== undefined && (!Array.isArray(spec.features) || spec.features.some((f) => typeof f !== "string" || !/^[A-Za-z0-9_,:-]+$/.test(f)))) fail("targets.mac.macPackage.features must be Cargo feature names");
|
|
198
|
+
if (spec.triple !== undefined && !MAC_TRIPLES.includes(spec.triple)) fail(`targets.mac.macPackage.triple must be one of ${MAC_TRIPLES.join(", ")}`);
|
|
69
199
|
const cwd = spec.tauriCwd ?? ".";
|
|
70
200
|
if (typeof cwd !== "string" || path.isAbsolute(cwd) || cwd.split(/[\\/]/).includes("..")) fail("targets.mac.macPackage.tauriCwd must be a relative path inside the app");
|
|
201
|
+
const cleanupSpec = macCleanupSpec(spec.cleanup);
|
|
202
|
+
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");
|
|
203
|
+
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");
|
|
71
204
|
// Tauri's universal-apple-darwin target builds both slices and lipos them itself.
|
|
72
|
-
const triple = spec.universal === false ? (process.arch === "arm64" ? "aarch64-apple-darwin" : "x86_64-apple-darwin") : "universal-apple-darwin";
|
|
205
|
+
const triple = spec.triple ?? (spec.universal === false ? (process.arch === "arm64" ? "aarch64-apple-darwin" : "x86_64-apple-darwin") : "universal-apple-darwin");
|
|
206
|
+
if (spec.triple !== undefined) {
|
|
207
|
+
const wrong = [...MAC_TRIPLES, "universal-apple-darwin"].find((other) => other !== triple && target.notarize.app.replaceAll("\\", "/").split("/").includes(other));
|
|
208
|
+
if (wrong) fail(`targets.mac.macPackage.triple is ${triple} but notarize.app is under ${wrong}; the built .app lives in the ${triple} directory`);
|
|
209
|
+
}
|
|
73
210
|
const override = { bundle: { createUpdaterArtifacts: false, macOS: { signingIdentity: null } } };
|
|
74
|
-
const
|
|
211
|
+
const extras = buildExtras(spec, "targets.mac.macPackage", { postBuild: true });
|
|
212
|
+
const args = ["exec", "tauri", "build", "--target", triple, "--bundles", "app", "--no-sign", ...configArgs(extras, appRoot, cwd), "--config", JSON.stringify(override)];
|
|
75
213
|
if (spec.features?.length) args.push("--features", spec.features.join(","));
|
|
76
|
-
|
|
77
|
-
|
|
214
|
+
args.push(...profileArgs(extras));
|
|
215
|
+
const mapPath = targetRootMapper({ spec, appRoot, cwd, env, resolveTargetRoot, label: "targets.mac.macPackage" });
|
|
216
|
+
const app = mapPath(target.notarize.app);
|
|
217
|
+
const entryFile = (entry) => (typeof entry === "string" ? entry : entry?.file);
|
|
218
|
+
const declaredFiles = target.sign?.prePackageFiles ?? [target.notarize.app];
|
|
219
|
+
if (!declaredFiles.some((entry) => entryFile(entry) === target.notarize.app)) fail("targets.mac.macPackage: sign.prePackageFiles must include notarize.app");
|
|
220
|
+
const prePackageFiles = declaredFiles.map((entry) => (typeof entry === "string" ? mapPath(entry) : { ...entry, file: mapPath(entry.file) }));
|
|
221
|
+
// Built-in cleanup before the build: the bundle dir is where a stale .app lives; DMG volumes are named after the product.
|
|
222
|
+
const appPosix = app.replaceAll("\\", "/");
|
|
223
|
+
const productName = path.posix.basename(appPosix, ".app");
|
|
224
|
+
const volumes = [...new Set([productName, ...(isObject(target.dmg) && typeof target.dmg.volname === "string" ? [target.dmg.volname] : [])])];
|
|
225
|
+
const cleanup = cleanupSpec.staleApps || cleanupSpec.staleDmgVolumes || cleanupSpec.lsclean
|
|
226
|
+
? {
|
|
227
|
+
...(cleanupSpec.staleDmgVolumes ? { dmgVolumes: volumes } : {}),
|
|
228
|
+
...(cleanupSpec.staleApps || cleanupSpec.lsclean ? { apps: { dir: path.posix.dirname(appPosix), remove: cleanupSpec.staleApps === true, unregister: cleanupSpec.lsclean === true } } : {}),
|
|
229
|
+
}
|
|
230
|
+
: undefined;
|
|
231
|
+
const wrapped = extras.prepare.length || extras.postBuild.length || cleanup;
|
|
78
232
|
const { macPackage, recipe: _ignoredMac, ...rest } = target;
|
|
233
|
+
// The updater tarball is made from the signed .app, so it runs in postSign, ahead of any postSign the app declares.
|
|
234
|
+
const updaterOutput = spec.updater === true ? `${productName}.app.tar.gz` : typeof spec.updater === "string" ? relativeInside(spec.updater, "targets.mac.macPackage.updater") : undefined;
|
|
235
|
+
const postSign = updaterOutput === undefined ? {} : {
|
|
236
|
+
postSign: [
|
|
237
|
+
{ cmd: process.execPath, args: [CREATE_MAC_UPDATER, "--app", app, "--output", updaterOutput] },
|
|
238
|
+
...(target.postSign === undefined ? [] : Array.isArray(target.postSign) ? target.postSign : [target.postSign]),
|
|
239
|
+
],
|
|
240
|
+
};
|
|
79
241
|
return {
|
|
80
242
|
...rest,
|
|
81
|
-
|
|
243
|
+
...postSign,
|
|
244
|
+
prePackage: wrapped ? recipeBuildCommand({ extras, build: { cmd: "pnpm", args, cwd }, env: QUIET_TAURI_ENV, cleanup }) : { cmd: "pnpm", args, cwd, env: QUIET_TAURI_ENV },
|
|
82
245
|
sign: { ...(target.sign ?? {}), prePackageFiles },
|
|
83
|
-
notarize: { ...target.notarize, appBeforePackage: true },
|
|
246
|
+
notarize: { ...target.notarize, app, appBeforePackage: true },
|
|
84
247
|
dmg: isObject(target.dmg) ? target.dmg : {},
|
|
85
248
|
recipe: { kind: "macPackage", universal: triple === "universal-apple-darwin" },
|
|
86
249
|
};
|
|
87
250
|
}
|
|
88
251
|
|
|
89
252
|
/** Expand whichever declarative package key the target carries. */
|
|
90
|
-
export function expandPackageRecipeTarget(target, { platform, appRoot, config }) {
|
|
91
|
-
|
|
92
|
-
if (platform === "
|
|
253
|
+
export function expandPackageRecipeTarget(target, { platform, appRoot, config, env, resolveTargetRoot }) {
|
|
254
|
+
const injected = { ...(env ? { env } : {}), ...(resolveTargetRoot ? { resolveTargetRoot } : {}) };
|
|
255
|
+
if (platform === "win") return expandWinPackageTarget(target, { appRoot, config, ...injected });
|
|
256
|
+
if (platform === "mac") return expandMacPackageTarget(target, { appRoot, config, ...injected });
|
|
93
257
|
if (target?.winPackage || target?.macPackage) fail(`winPackage / macPackage are not valid on ${platform}`);
|
|
94
258
|
return target;
|
|
95
259
|
}
|
package/package.json
CHANGED