@rightkit/release 0.2.135 → 0.2.137
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 +62 -0
- package/addon-command.mjs +53 -34
- package/apple-profile-renew.mjs +30 -12
- package/artifact-storage.mjs +47 -10
- package/background-process.mjs +18 -0
- package/build-release.mjs +1 -1
- package/cache-command.mjs +4 -3
- package/cargo-contract.mjs +1 -1
- package/cargo-guard.mjs +4 -3
- package/cargo-registry-readback.mjs +51 -0
- package/cargo-target.mjs +3 -1
- package/cli/right-release.mjs +26 -0
- package/codesign-environment.mjs +26 -2
- package/codesign-shim.mjs +2 -1
- package/committed-source.mjs +3 -3
- package/development.mjs +7 -7
- package/direct-bootstrap.mjs +19 -9
- package/dmg-build.mjs +117 -0
- package/dmg-layout.mjs +123 -0
- package/github-release.mjs +74 -37
- package/heavy-command.mjs +3 -2
- package/install-app.mjs +358 -0
- package/inventory-check.mjs +980 -0
- package/macos-signing-session.mjs +3 -0
- package/managed-cargo.mjs +23 -19
- package/managed-host-secrets.mjs +30 -0
- package/managed-roots.mjs +39 -0
- package/model-promote.mjs +31 -37
- package/notary-auth.mjs +4 -0
- package/osascript-guard.mjs +24 -0
- package/ownership-approval-command.mjs +15 -4
- package/ownership-approval.mjs +114 -49
- package/ownership-availability.mjs +25 -0
- package/ownership-catalog-config.json +18 -0
- package/ownership-catalog.mjs +94 -0
- package/ownership-command.mjs +20 -10
- package/ownership-declaration.json +15 -0
- package/ownership-default-policy.json +1 -11
- package/ownership-evidence.mjs +10 -8
- package/ownership-git-inventory.mjs +29 -1
- package/ownership-helper-paths.json +2 -2
- package/ownership-hooks.mjs +107 -0
- package/ownership-internal-owners.json +44 -0
- package/ownership-key-authority.mjs +112 -7
- package/ownership-key-command.mjs +76 -19
- package/ownership-key-trust.mjs +97 -9
- package/ownership-lockfiles.mjs +311 -0
- package/ownership-report-command.mjs +12 -3
- package/ownership-report.mjs +20 -17
- package/ownership-repositories.json +19 -0
- package/ownership-scan-worker.mjs +96 -11
- package/ownership-source.mjs +39 -0
- package/ownership-staged.mjs +193 -0
- package/ownership-verdict.mjs +74 -0
- package/ownership.mjs +215 -107
- package/package.json +1 -1
- package/protected-credentials.mjs +111 -0
- package/prune-r2.mjs +4 -3
- package/pub-upload.mjs +3 -85
- package/publication-artifacts.mjs +108 -0
- package/publication-gate.mjs +28 -7
- package/publish-cargo.mjs +47 -16
- package/publish-swift.mjs +46 -17
- package/publish-update.mjs +61 -39
- package/r2-verified-upload.mjs +41 -0
- package/registry-parity.mjs +5 -3
- package/release-state.mjs +1 -0
- package/release-token.mjs +2 -0
- package/rightkit-drift.mjs +3 -2
- package/rightkit-ownership.json +2028 -139
- package/rightkit-signing-agent.mjs +5 -3
- package/rightkit-sync.mjs +6 -2
- package/rightkit-versions.json +215 -103
- package/runtime-artifact-manifest.mjs +20 -6
- package/sign-updater.mjs +5 -0
- package/signing-agent-client.mjs +2 -1
- package/signing-agent-install.mjs +3 -2
- package/standalone-clone-verify.mjs +3 -2
- package/tauri-bundle-marker.mjs +3 -2
- package/upload-large.mjs +36 -82
- package/verifier.mjs +5 -1
package/codesign-environment.mjs
CHANGED
|
@@ -20,8 +20,21 @@ export function macosCodesignEnvironment(env = process.env, { platform = process
|
|
|
20
20
|
if (platform !== "darwin") return env;
|
|
21
21
|
const route = detect({ env }).guiRunner ? "agent" : "direct";
|
|
22
22
|
const script = codesignShimScript({ route, node });
|
|
23
|
+
const automationScript = `#!/bin/sh\nexec ${quote(node)} ${quote(fileURLToPath(new URL("./osascript-guard.mjs", import.meta.url)))} "$@"\n`;
|
|
24
|
+
// Tauri's script invokes /usr/bin/osascript, bypassing PATH. Its env-bash
|
|
25
|
+
// shebang & app-side `bash bundle_dmg.sh` calls both traverse this wrapper.
|
|
26
|
+
const bashScript = `#!/bin/sh\nlayout_script=\nskip_layout=0\nfor arg in "$@"; do
|
|
27
|
+
case "$arg" in
|
|
28
|
+
*/bundle_dmg.sh|bundle_dmg.sh) layout_script="$arg" ;;
|
|
29
|
+
--skip-jenkins) skip_layout=1 ;;
|
|
30
|
+
esac
|
|
31
|
+
done
|
|
32
|
+
if [ -n "$layout_script" ] && [ "$skip_layout" = 0 ]; then
|
|
33
|
+
exec ${quote(node)} ${quote(fileURLToPath(new URL("./osascript-guard.mjs", import.meta.url)))} --caller "$layout_script"
|
|
34
|
+
fi
|
|
35
|
+
exec /bin/bash "$@"\n`;
|
|
23
36
|
// Immutable wrappers avoid races between GUI & SSH builds or package versions.
|
|
24
|
-
const identity = createHash("sha256").update(script).digest("hex").slice(0, 24);
|
|
37
|
+
const identity = createHash("sha256").update(script).update(automationScript).update(bashScript).digest("hex").slice(0, 24);
|
|
25
38
|
const bin = path.join(resolveManagedRoot({ env }), "signing", "codesign-bin", identity);
|
|
26
39
|
ensurePrivateDirectory(bin, { io });
|
|
27
40
|
const file = path.join(bin, "codesign");
|
|
@@ -29,6 +42,17 @@ export function macosCodesignEnvironment(env = process.env, { platform = process
|
|
|
29
42
|
privatePath(file, 0o700, { io });
|
|
30
43
|
if (io.readFileSync(file, "utf8") !== script) throw new Error("Managed codesign shim content changed");
|
|
31
44
|
} else io.writeFileSync(file, script, { mode: 0o700, flag: "wx" });
|
|
45
|
+
const automationFile = path.join(bin, "osascript");
|
|
46
|
+
if (io.existsSync(automationFile)) {
|
|
47
|
+
privatePath(automationFile, 0o700, { io });
|
|
48
|
+
if (io.readFileSync(automationFile, "utf8") !== automationScript) throw new Error("Managed osascript guard content changed");
|
|
49
|
+
} else io.writeFileSync(automationFile, automationScript, { mode: 0o700, flag: "wx" });
|
|
50
|
+
const bashFile = path.join(bin, "bash");
|
|
51
|
+
if (io.existsSync(bashFile)) {
|
|
52
|
+
privatePath(bashFile, 0o700, { io });
|
|
53
|
+
if (io.readFileSync(bashFile, "utf8") !== bashScript) throw new Error("Managed bash guard content changed");
|
|
54
|
+
} else io.writeFileSync(bashFile, bashScript, { mode: 0o700, flag: "wx" });
|
|
32
55
|
// Keep every other shim (especially managed Cargo) in its original order.
|
|
33
|
-
return { ...env,
|
|
56
|
+
return { ...env, CI: "true", TAURI_BUNDLER_DMG_IGNORE_CI: "false",
|
|
57
|
+
PATH: [bin, ...(env.PATH ?? "").split(path.delimiter).filter((entry) => entry !== bin)].join(path.delimiter) };
|
|
34
58
|
}
|
package/codesign-shim.mjs
CHANGED
|
@@ -35,7 +35,8 @@ export async function runCodesignShim(argv, { cwd = process.cwd(), env = process
|
|
|
35
35
|
return result;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
const realOrSelf = (file) => { try { return fs.realpathSync(file); } catch { return path.resolve(file); } };
|
|
39
|
+
if (process.argv[1] && realOrSelf(process.argv[1]) === realOrSelf(fileURLToPath(import.meta.url))) {
|
|
39
40
|
try {
|
|
40
41
|
// Only the managed shell wrapper invokes this entry: session detection was
|
|
41
42
|
// already performed by right-release before selecting its wrapper directory.
|
package/committed-source.mjs
CHANGED
|
@@ -35,7 +35,7 @@ function gitEnv(env = process.env, platform = process.platform) {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
function runGit(root, args, { encoding = "buffer", maxBuffer = 4 * 1024 * 1024, timeout = COMMAND_TIMEOUT_MS, runner = spawnSync, env = gitEnv(), input } = {}) {
|
|
38
|
-
const result = runner(nativeGitPath(), ["-C", root, "--no-replace-objects", "-c", "core.fsmonitor=false", "-c", `core.hooksPath=${os.devNull}`, ...args], {
|
|
38
|
+
const result = runner(nativeGitPath(), ["-C", root, "--no-replace-objects", "-c", "core.fsmonitor=false", "-c", `core.hooksPath=${os.devNull}`, "-c", `safe.directory=${root}`, ...args], {
|
|
39
39
|
encoding, env, windowsHide: true, maxBuffer, timeout, ...(input ? { input } : {}),
|
|
40
40
|
});
|
|
41
41
|
if (result?.error || result?.status !== 0) throw new Error(`committed ownership: git ${args[0] ?? "command"} failed${result?.error?.code ? ` (${result.error.code})` : ""}`);
|
|
@@ -118,7 +118,7 @@ function acquire(root, { expectedCommit, expectedTree, requiredPaths = [], runne
|
|
|
118
118
|
const text = (args) => runGit(root, args, { encoding: "utf8", runner, env }).trim();
|
|
119
119
|
const repositoryRoot = path.resolve(text(["rev-parse", "--show-toplevel"]));
|
|
120
120
|
const pathPrefix = text(["rev-parse", "--show-prefix"]).replaceAll("\\", "/");
|
|
121
|
-
const originResult = runner(nativeGitPath(), ["-C", repositoryRoot, "--no-replace-objects", "-c", "core.fsmonitor=false", "-c", `core.hooksPath=${os.devNull}`, "config", "--get", "remote.origin.url"], {
|
|
121
|
+
const originResult = runner(nativeGitPath(), ["-C", repositoryRoot, "--no-replace-objects", "-c", "core.fsmonitor=false", "-c", `core.hooksPath=${os.devNull}`, "-c", `safe.directory=${repositoryRoot}`, "config", "--get", "remote.origin.url"], {
|
|
122
122
|
encoding: "utf8", env, windowsHide: true, maxBuffer: 4 * 1024 * 1024, timeout: COMMAND_TIMEOUT_MS,
|
|
123
123
|
});
|
|
124
124
|
const origin = originResult?.status === 0 ? String(originResult.stdout).trim() : null;
|
|
@@ -132,7 +132,7 @@ function acquire(root, { expectedCommit, expectedTree, requiredPaths = [], runne
|
|
|
132
132
|
const rows = parseTree(runGit(root, ["ls-tree", "-r", "-z", "--full-tree", tree], { encoding: "buffer", maxBuffer: MAX_TREE_BYTES, timeout: COMMAND_TIMEOUT_MS, runner, env }), objectFormat);
|
|
133
133
|
const wanted = new Set(requiredPaths.filter(validatePath));
|
|
134
134
|
const shouldRead = (row) => row.type === "blob" && row.mode !== "120000" && (
|
|
135
|
-
wanted.has(row.rel) || ["Cargo.toml", "package.json", "Package.swift", "ownership-exceptions.json"].includes(path.posix.basename(row.rel)) ||
|
|
135
|
+
wanted.has(row.rel) || ["Cargo.toml", "package.json", "Package.swift", "ownership-exceptions.json", "Cargo.lock", "pnpm-lock.yaml", "package-lock.json", "npm-shrinkwrap.json", "yarn.lock"].includes(path.posix.basename(row.rel)) ||
|
|
136
136
|
/\.(?:rs|ts|tsx|js|mjs|cjs|jsx|swift|m|mm|c|cc|cpp|h|py|go|kt|java|cs)$/i.test(row.rel)
|
|
137
137
|
);
|
|
138
138
|
const candidates = rows.filter(shouldRead);
|
package/development.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { discoverBrokerSocket, resolveManagedRoots } from './managed-roots.mjs';
|
|
1
2
|
import { spawnSync } from "./background-process.mjs";
|
|
2
3
|
import { createHash } from "node:crypto";
|
|
3
4
|
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync } from "node:fs";
|
|
4
5
|
import path from "node:path";
|
|
5
|
-
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
7
|
import { assertPrimaryReleaseCheckout } from "./release-invocation.mjs";
|
|
7
8
|
import { assertReleaseExecutionAllowed } from "./execution-policy.mjs";
|
|
8
9
|
import { acquireManagedRunLease, managedRuntimeClient, runCargoSync } from "./managed-cargo.mjs";
|
|
@@ -27,16 +28,14 @@ export function developmentEnvironment(env = process.env) {
|
|
|
27
28
|
delete result.CARGO_INCREMENTAL;
|
|
28
29
|
const client = managedRuntimeClient(result);
|
|
29
30
|
if (client) {
|
|
30
|
-
const
|
|
31
|
-
?? (process.platform === "win32" ? "D:\\.rightkit-managed" : process.platform === "darwin" ? "/Volumes/D/.rightkit-managed" : undefined);
|
|
32
|
-
const agentBin = managedRoot && path.join(managedRoot, "agent-bin");
|
|
31
|
+
const { agentBin } = resolveManagedRoots({ env: result });
|
|
33
32
|
if (agentBin && existsSync(agentBin)) {
|
|
34
33
|
const pathKey = Object.keys(result).find((key) => key.toLowerCase() === "path") ?? "PATH";
|
|
35
34
|
result[pathKey] = [agentBin, result[pathKey]].filter(Boolean).join(path.delimiter);
|
|
36
35
|
}
|
|
37
36
|
// Same default as the agent-bin `rightkit` shim: children that run the managed client directly
|
|
38
37
|
// (resolveCommand("rightkit"), build scripts) must not lose the broker endpoint.
|
|
39
|
-
const socket =
|
|
38
|
+
const socket = discoverBrokerSocket(result);
|
|
40
39
|
if (!result.RIGHTKIT_BUILD_BROKER_SOCKET && socket && existsSync(socket)) result.RIGHTKIT_BUILD_BROKER_SOCKET = socket;
|
|
41
40
|
}
|
|
42
41
|
return result;
|
|
@@ -212,7 +211,7 @@ export function resolveCommand(command, args, { root, env = process.env } = {})
|
|
|
212
211
|
if (command === "rightkit") {
|
|
213
212
|
const client = managedRuntimeClient(env);
|
|
214
213
|
if (!client) throw new Error("installed RightKit runtime client is unavailable for development command");
|
|
215
|
-
const socket =
|
|
214
|
+
const socket = discoverBrokerSocket(env);
|
|
216
215
|
const childEnv = !env.RIGHTKIT_BUILD_BROKER_SOCKET && existsSync(socket) ? { ...env, RIGHTKIT_BUILD_BROKER_SOCKET: socket } : undefined;
|
|
217
216
|
return { command: process.execPath, args: [client, ...args], ...(childEnv ? { env: childEnv } : {}) };
|
|
218
217
|
}
|
|
@@ -282,6 +281,7 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
282
281
|
return result;
|
|
283
282
|
}
|
|
284
283
|
|
|
285
|
-
|
|
284
|
+
const realOrSelf = (file) => { try { return realpathSync(file); } catch { return path.resolve(file); } };
|
|
285
|
+
if (process.argv[1] && realOrSelf(process.argv[1]) === realOrSelf(fileURLToPath(import.meta.url))) {
|
|
286
286
|
main().catch((error) => { console.error(`right-release dev: ${error.code ? `[${error.code}] ` : ""}${error.message}`); process.exitCode = 1; });
|
|
287
287
|
}
|
package/direct-bootstrap.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { readVerifiedArtifact } from "./publication-artifacts.mjs";
|
|
1
2
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { mkdirSync, readFileSync, rmSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
|
3
4
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
5
|
import { spawnSync } from "./background-process.mjs";
|
|
5
6
|
import { RIGHTRELEASE_MANIFEST_SIGNER, signReleaseManifestWithAzure } from "./sign-release-manifest.mjs";
|
|
@@ -539,7 +540,8 @@ export function planBootstrapPublication({ product, bootstrapVersion, scriptPath
|
|
|
539
540
|
if (stableKey !== `${product}/install.ps1`) throw new Error("bootstrap stable key must be product/install.ps1");
|
|
540
541
|
const script = statSync(scriptPath);
|
|
541
542
|
if (!script.isFile() || script.size < 1 || script.size > MAX_BOOTSTRAP_BYTES || scriptPath.split(/[\\/]/).at(-1) !== "install.ps1") throw new Error("bootstrap must be one small install.ps1 file");
|
|
542
|
-
const
|
|
543
|
+
const scriptBytes = readFileSync(scriptPath);
|
|
544
|
+
const validation = validatePowerShellBootstrap(scriptBytes.toString("utf8"), { product });
|
|
543
545
|
if (!validation.valid) throw new Error("bootstrap script violates generated RightRelease contract: " + validation.errors.join("; "));
|
|
544
546
|
const versionedKey = `${product}/versions/${bootstrapVersion}/install.ps1`;
|
|
545
547
|
return {
|
|
@@ -549,7 +551,7 @@ export function planBootstrapPublication({ product, bootstrapVersion, scriptPath
|
|
|
549
551
|
stableKey,
|
|
550
552
|
versionedKey,
|
|
551
553
|
scriptPath: resolve(scriptPath),
|
|
552
|
-
sha256:
|
|
554
|
+
sha256: createHash("sha256").update(scriptBytes).digest("hex"),
|
|
553
555
|
};
|
|
554
556
|
}
|
|
555
557
|
|
|
@@ -567,16 +569,24 @@ export function createWranglerR2Client({ commandRunner = spawnSync, wranglerRunn
|
|
|
567
569
|
|
|
568
570
|
export function publishBootstrapPlan(plan, r2, { verificationPath }) {
|
|
569
571
|
validateBootstrapPublicationPlan(plan);
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
572
|
+
readVerifiedArtifact(plan.scriptPath, { sha256: plan.sha256 });
|
|
573
|
+
for (const [label, key] of [["immutable", plan.versionedKey], ["stable", plan.stableKey]]) {
|
|
574
|
+
try { r2.put(plan.bucket, key, plan.scriptPath); }
|
|
575
|
+
catch { throw new Error(`bootstrap upload failed; inspect remote key ${plan.bucket}/${key}`); }
|
|
576
|
+
// A successful readback must materialize fresh bytes, never reuse prior output.
|
|
577
|
+
rmSync(verificationPath, { force: true });
|
|
578
|
+
try {
|
|
579
|
+
r2.get(plan.bucket, key, verificationPath);
|
|
580
|
+
readVerifiedArtifact(verificationPath, { sha256: plan.sha256 });
|
|
581
|
+
} catch {
|
|
582
|
+
throw new Error(`${label} bootstrap remote verification failed; remove remote key ${plan.bucket}/${key}`);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
576
585
|
return { ...plan, verified: true };
|
|
577
586
|
}
|
|
578
587
|
|
|
579
588
|
export function validateBootstrapPublicationPlan(plan) {
|
|
589
|
+
if (!/^[a-f0-9]{64}$/.test(plan?.sha256 ?? "")) throw new Error("bootstrap publication hash evidence is required");
|
|
580
590
|
if (!plan || !/^[a-z][a-z0-9-]+$/.test(plan.product ?? "")) throw new Error("bootstrap publication product is invalid");
|
|
581
591
|
if (!VERSION.test(plan.bootstrapVersion ?? "")) throw new Error("bootstrap publication version is invalid");
|
|
582
592
|
if (plan.bucket !== "rightapps-downloads") throw new Error("bootstrap publication bucket is not branded public R2");
|
package/dmg-build.mjs
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { copyFileSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync, realpathSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { spawnSync } from "./background-process.mjs";
|
|
5
|
+
import { backgroundAlias, dmgDSStore } from "./dmg-layout.mjs";
|
|
6
|
+
import { assertReleaseExecutionAllowed } from "./execution-policy.mjs";
|
|
7
|
+
|
|
8
|
+
const fail = (code, message) => Object.assign(new Error(message), { code });
|
|
9
|
+
const pair = (value, label, positive = false) => {
|
|
10
|
+
const numbers = Array.isArray(value) ? value : String(value).split(",").map(Number);
|
|
11
|
+
if (numbers.length !== 2 || numbers.some((n) => !Number.isInteger(n) || n < (positive ? 1 : 0) || n > 32767)) {
|
|
12
|
+
throw fail("DMG_INVALID_OPTIONS", `${label} requires ${positive ? "positive" : "nonnegative"} x,y integers <=32767`);
|
|
13
|
+
}
|
|
14
|
+
return numbers;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function buildDmg({ app, out, volname, background, iconSize = 128, appPosition = [158, 231],
|
|
18
|
+
applicationsPosition = [492, 231], window = [660, 400], volumeIcon } = {},
|
|
19
|
+
{ platform = process.platform, run = spawnSync, makeAlias = backgroundAlias,
|
|
20
|
+
log = (event) => console.error(JSON.stringify(event)) } = {}) {
|
|
21
|
+
if (platform !== "darwin") throw fail("DMG_NATIVE_HOST_REQUIRED", "DMG creation requires macOS");
|
|
22
|
+
if (!app || !out || !volname) throw fail("DMG_INVALID_OPTIONS", "--app, --out & --volname are required");
|
|
23
|
+
app = path.resolve(app); out = path.resolve(out);
|
|
24
|
+
if (!app.endsWith(".app") || !statSync(app).isDirectory() || !existsSync(path.join(app, "Contents", "Info.plist"))) {
|
|
25
|
+
throw fail("DMG_INVALID_OPTIONS", `expected .app bundle with Contents/Info.plist: ${app}`);
|
|
26
|
+
}
|
|
27
|
+
if (!out.endsWith(".dmg") || out.startsWith(`${app}${path.sep}`)) throw fail("DMG_INVALID_OPTIONS", "--out must be a .dmg outside app bundle");
|
|
28
|
+
if (/[\0\r\n/:]/.test(volname) || Buffer.byteLength(volname) > 255) throw fail("DMG_INVALID_OPTIONS", "invalid volume name");
|
|
29
|
+
iconSize = Number(iconSize);
|
|
30
|
+
if (!Number.isInteger(iconSize) || iconSize < 16 || iconSize > 512) throw fail("DMG_INVALID_OPTIONS", "--icon-size must be 16..512");
|
|
31
|
+
appPosition = pair(appPosition, "--app-position"); applicationsPosition = pair(applicationsPosition, "--applications-position");
|
|
32
|
+
window = pair(window, "--window", true);
|
|
33
|
+
if (volumeIcon) {
|
|
34
|
+
volumeIcon = path.resolve(volumeIcon);
|
|
35
|
+
if (!existsSync(volumeIcon) || !statSync(volumeIcon).isFile() || !/\.icns$/i.test(volumeIcon)) throw fail("DMG_INVALID_OPTIONS", "--volume-icon must be an .icns file");
|
|
36
|
+
}
|
|
37
|
+
if (background) {
|
|
38
|
+
background = path.resolve(background);
|
|
39
|
+
// Existing Right Suite backgrounds include TIFF; accept PNG & TIFF without conversion.
|
|
40
|
+
if (!statSync(background).isFile() || !/\.(png|tiff?)$/i.test(background)) throw fail("DMG_INVALID_OPTIONS", "background must be PNG or TIFF");
|
|
41
|
+
if (/\.png$/i.test(background) && !readFileSync(background).subarray(0, 8).equals(Buffer.from("89504e470d0a1a0a", "hex"))) {
|
|
42
|
+
throw fail("DMG_INVALID_OPTIONS", "invalid PNG signature");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
for (let cursor = out; cursor !== path.dirname(cursor); cursor = path.dirname(cursor)) {
|
|
46
|
+
if (existsSync(cursor) && lstatSync(cursor).isSymbolicLink()) throw fail("DMG_INVALID_OPTIONS", `output must not traverse symlink: ${cursor}`);
|
|
47
|
+
}
|
|
48
|
+
assertReleaseExecutionAllowed({ cwd: app, command: "right-release dmg build" });
|
|
49
|
+
mkdirSync(path.dirname(out), { recursive: true });
|
|
50
|
+
const work = mkdtempSync(path.join(path.dirname(out), ".rightkit-dmg-")), stage = path.join(work, "stage"), mount = path.join(work, "mount");
|
|
51
|
+
const rw = path.join(work, "writable.dmg"), compressed = path.join(work, "compressed.dmg");
|
|
52
|
+
let attached = false, attaching = false;
|
|
53
|
+
const command = (tool, args) => {
|
|
54
|
+
const result = run(tool, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], shell: false, timeout: 300000 });
|
|
55
|
+
if (result.error || result.status !== 0) throw fail("DMG_COMMAND_FAILED", `${tool} ${args[0]} failed: ${result.error?.message ?? result.stderr ?? result.status}`);
|
|
56
|
+
return result;
|
|
57
|
+
};
|
|
58
|
+
log({ event: "dmg-build-started", app, out, volname });
|
|
59
|
+
try {
|
|
60
|
+
mkdirSync(stage); mkdirSync(mount);
|
|
61
|
+
command("/usr/bin/ditto", [app, path.join(stage, path.basename(app))]);
|
|
62
|
+
symlinkSync("/Applications", path.join(stage, "Applications"));
|
|
63
|
+
const relative = background ? path.join(".background", path.basename(background)) : null;
|
|
64
|
+
if (relative) { mkdirSync(path.join(stage, ".background")); copyFileSync(background, path.join(stage, relative)); }
|
|
65
|
+
// Include layout headroom in hdiutil's source-folder size estimate. Alias
|
|
66
|
+
// CNIDs become available only after mounting, when this reservation is replaced.
|
|
67
|
+
writeFileSync(path.join(stage, ".DS_Store"), Buffer.alloc(64 * 1024));
|
|
68
|
+
command("/usr/bin/hdiutil", ["create", "-fs", "HFS+", "-format", "UDRW", "-volname", volname, "-srcfolder", stage, rw]);
|
|
69
|
+
attaching = true;
|
|
70
|
+
command("/usr/bin/hdiutil", ["attach", "-readwrite", "-nobrowse", "-noautoopen", "-mountpoint", mount, rw]);
|
|
71
|
+
attached = true; attaching = false;
|
|
72
|
+
if (volumeIcon) {
|
|
73
|
+
// Custom volume icon: .VolumeIcon.icns at the root plus the volume's kHasCustomIcon (0x0400) Finder flag.
|
|
74
|
+
copyFileSync(volumeIcon, path.join(mount, ".VolumeIcon.icns"));
|
|
75
|
+
const finderInfo = Buffer.alloc(32); finderInfo.writeUInt16BE(0x0400, 8);
|
|
76
|
+
command("/usr/bin/xattr", ["-wx", "com.apple.FinderInfo", finderInfo.toString("hex"), mount]);
|
|
77
|
+
}
|
|
78
|
+
const alias = relative ? makeAlias({ mount, volname, relative }) : undefined;
|
|
79
|
+
writeFileSync(path.join(mount, ".DS_Store"), dmgDSStore({ appName: path.basename(app), iconSize,
|
|
80
|
+
appPosition, applicationsPosition, window, alias }));
|
|
81
|
+
command("/usr/bin/hdiutil", ["detach", mount]); attached = false;
|
|
82
|
+
command("/usr/bin/hdiutil", ["convert", rw, "-format", "UDZO", "-imagekey", "zlib-level=9", "-o", compressed]);
|
|
83
|
+
command("/usr/bin/hdiutil", ["verify", compressed]);
|
|
84
|
+
renameSync(compressed, out);
|
|
85
|
+
log({ event: "dmg-build-completed", out, format: "UDZO" });
|
|
86
|
+
return { out, format: "UDZO" };
|
|
87
|
+
} catch (error) {
|
|
88
|
+
log({ event: "dmg-build-failed", out, code: error.code ?? "DMG_BUILD_FAILED", message: error.message });
|
|
89
|
+
throw error;
|
|
90
|
+
} finally {
|
|
91
|
+
if (attached || attaching) {
|
|
92
|
+
try { command("/usr/bin/hdiutil", ["detach", "-force", mount]); attached = false; attaching = false; }
|
|
93
|
+
catch (error) { log({ event: "dmg-cleanup-failed", mount, work, message: error.message }); }
|
|
94
|
+
}
|
|
95
|
+
// Never recurse through a mount that failed to detach.
|
|
96
|
+
if (!attached && !attaching) rmSync(work, { recursive: true, force: true });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function parseDmgArguments(args) {
|
|
101
|
+
if (args[0] !== "build") throw fail("DMG_INVALID_OPTIONS", "expected dmg build");
|
|
102
|
+
const flags = new Map([["--app", "app"], ["--out", "out"], ["--volname", "volname"], ["--background", "background"],
|
|
103
|
+
["--icon-size", "iconSize"], ["--app-position", "appPosition"], ["--applications-position", "applicationsPosition"], ["--window", "window"], ["--volume-icon", "volumeIcon"]]);
|
|
104
|
+
const options = {};
|
|
105
|
+
for (let i = 1; i < args.length; i += 2) {
|
|
106
|
+
const key = flags.get(args[i]);
|
|
107
|
+
if (!key || options[key] !== undefined || !args[i + 1] || args[i + 1].startsWith("--")) throw fail("DMG_INVALID_OPTIONS", `invalid option: ${args[i]}`);
|
|
108
|
+
options[key] = args[i + 1];
|
|
109
|
+
}
|
|
110
|
+
return options;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const realOrSelf = (file) => { try { return realpathSync(file); } catch { return path.resolve(file); } };
|
|
114
|
+
if (process.argv[1] && realOrSelf(process.argv[1]) === realOrSelf(fileURLToPath(import.meta.url))) {
|
|
115
|
+
try { buildDmg(parseDmgArguments(process.argv.slice(2))); }
|
|
116
|
+
catch (error) { console.error(`right-release dmg: [${error.code ?? "DMG_BUILD_FAILED"}] ${error.message}`); process.exitCode = 1; }
|
|
117
|
+
}
|
package/dmg-layout.mjs
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Finder records follow dmgbuild's ds_store/mac_alias formats. No Finder IPC.
|
|
2
|
+
// https://github.com/dmgbuild/ds_store/tree/main/src/ds_store
|
|
3
|
+
// https://github.com/dmgbuild/mac_alias/blob/main/src/mac_alias/alias.py
|
|
4
|
+
import { statSync } from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
const u32 = (n) => { const b = Buffer.alloc(4); b.writeUInt32BE(n); return b; };
|
|
8
|
+
const u16 = (n) => { const b = Buffer.alloc(2); b.writeUInt16BE(n); return b; };
|
|
9
|
+
const utf16 = (s) => Buffer.from(s, "utf16le").swap16();
|
|
10
|
+
const uint = (n, size) => { const b = Buffer.alloc(size); b.writeUIntBE(n, 0, size); return b; };
|
|
11
|
+
|
|
12
|
+
// Small binary-plist encoder for Finder view dictionaries (strings, data, real,
|
|
13
|
+
// integer, boolean). Object references & offset widths are computed per plist.
|
|
14
|
+
export function finderPlist(value) {
|
|
15
|
+
const objects = [];
|
|
16
|
+
function add(v, key) {
|
|
17
|
+
const id = objects.length;
|
|
18
|
+
objects.push(null);
|
|
19
|
+
if (Buffer.isBuffer(v)) objects[id] = { kind: 0x40, bytes: v, length: v.length };
|
|
20
|
+
else if (typeof v === "string") objects[id] = { kind: 0x60, bytes: utf16(v), length: v.length };
|
|
21
|
+
else if (typeof v === "boolean") objects[id] = { bytes: Buffer.from([v ? 9 : 8]) };
|
|
22
|
+
else if (typeof v === "number") {
|
|
23
|
+
if (!Number.isFinite(v)) throw new TypeError("non-finite plist value");
|
|
24
|
+
const bytes = Buffer.alloc(9);
|
|
25
|
+
if (["viewOptionsVersion", "backgroundType", "SidebarWidth"].includes(key)) {
|
|
26
|
+
bytes[0] = 0x13; bytes.writeBigInt64BE(BigInt(v), 1);
|
|
27
|
+
} else { bytes[0] = 0x23; bytes.writeDoubleBE(v, 1); }
|
|
28
|
+
objects[id] = { bytes };
|
|
29
|
+
} else {
|
|
30
|
+
const entries = Object.entries(v);
|
|
31
|
+
objects[id] = { kind: 0xd0, length: entries.length,
|
|
32
|
+
refs: [...entries.map(([k]) => add(k)), ...entries.map(([k, val]) => add(val, k))] };
|
|
33
|
+
}
|
|
34
|
+
return id;
|
|
35
|
+
}
|
|
36
|
+
const root = add(value), refSize = objects.length < 256 ? 1 : 2;
|
|
37
|
+
const prefix = (kind, length) => length < 15 ? Buffer.from([kind | length])
|
|
38
|
+
: Buffer.concat([Buffer.from([kind | 15, 0x12]), u32(length)]);
|
|
39
|
+
const chunks = objects.map((o) => o.kind == null ? o.bytes : Buffer.concat([
|
|
40
|
+
prefix(o.kind, o.length), o.refs ? Buffer.concat(o.refs.map((ref) => uint(ref, refSize))) : o.bytes,
|
|
41
|
+
]));
|
|
42
|
+
let offset = 8;
|
|
43
|
+
const offsets = chunks.map((chunk) => { const start = offset; offset += chunk.length; return start; });
|
|
44
|
+
const offsetSize = offset < 256 ? 1 : offset < 65536 ? 2 : 4;
|
|
45
|
+
const trailer = Buffer.alloc(32);
|
|
46
|
+
trailer[6] = offsetSize; trailer[7] = refSize;
|
|
47
|
+
trailer.writeBigUInt64BE(BigInt(objects.length), 8);
|
|
48
|
+
trailer.writeBigUInt64BE(BigInt(root), 16);
|
|
49
|
+
trailer.writeBigUInt64BE(BigInt(offset), 24);
|
|
50
|
+
return Buffer.concat([Buffer.from("bplist00"), ...chunks, ...offsets.map((n) => uint(n, offsetSize)), trailer]);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Carbon Alias v2, with mounted HFS+ CNIDs, dates, Unicode & volume-relative paths. */
|
|
54
|
+
export function backgroundAlias({ mount, volname, relative, stat = statSync }) {
|
|
55
|
+
const target = path.join(mount, relative), file = stat(target), volume = stat(mount), folder = stat(path.dirname(target));
|
|
56
|
+
const macDate = (s) => Math.floor(s.birthtimeMs / 1000 + 2082844800);
|
|
57
|
+
const header = Buffer.alloc(150);
|
|
58
|
+
header.writeUInt16BE(2, 6); // version; kind=file
|
|
59
|
+
const pascal = (text, offset, size) => {
|
|
60
|
+
const bytes = Buffer.from(text.replaceAll(":", "/"));
|
|
61
|
+
header[offset] = Math.min(bytes.length, size - 1); bytes.copy(header, offset + 1, 0, size - 1);
|
|
62
|
+
};
|
|
63
|
+
pascal(volname, 10, 28); header.writeUInt32BE(macDate(volume), 38);
|
|
64
|
+
header.write("H+", 42, "ascii");
|
|
65
|
+
header.writeUInt32BE(folder.ino, 46); pascal(path.basename(target), 50, 64);
|
|
66
|
+
header.writeUInt32BE(file.ino, 114); header.writeUInt32BE(macDate(file), 118);
|
|
67
|
+
header.writeInt16BE(-1, 130); header.writeInt16BE(-1, 132);
|
|
68
|
+
const tag = (code, bytes) => Buffer.concat([u16(code), u16(bytes.length), bytes, ...(bytes.length % 2 ? [Buffer.alloc(1)] : [])]);
|
|
69
|
+
const unicode = (s) => Buffer.concat([u16(s.length), utf16(s)]);
|
|
70
|
+
const highDate = (s) => { const b = Buffer.alloc(8); b.writeBigUInt64BE(BigInt(Math.floor((s.birthtimeMs / 1000 + 2082844800) * 65536))); return b; };
|
|
71
|
+
const carbonPath = `${volname}:${relative.split(path.sep).map((s) => s.replaceAll(":", "/")).join(":\0")}`;
|
|
72
|
+
const bytes = Buffer.concat([header, tag(0, Buffer.from(path.basename(path.dirname(target)))),
|
|
73
|
+
tag(16, highDate(volume)), tag(17, highDate(file)), tag(1, u32(folder.ino)), tag(2, Buffer.from(carbonPath)),
|
|
74
|
+
tag(14, unicode(path.basename(target))), tag(15, unicode(volname)),
|
|
75
|
+
tag(18, Buffer.from(`/${relative.split(path.sep).join("/")}`)), tag(19, Buffer.from(mount)),
|
|
76
|
+
Buffer.from([255, 255, 0, 0])]);
|
|
77
|
+
if (bytes.length > 32767) throw new RangeError("background alias too large");
|
|
78
|
+
bytes.writeUInt16BE(bytes.length, 4);
|
|
79
|
+
return bytes;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** One sorted B-tree leaf in a valid Bud1 allocator, sufficient for DMG layout. */
|
|
83
|
+
export function dmgDSStore({ appName, iconSize = 128, appPosition = [158, 231],
|
|
84
|
+
applicationsPosition = [492, 231], window = [660, 400], alias } = {}) {
|
|
85
|
+
const entries = [];
|
|
86
|
+
const add = (name, code, type, bytes) => entries.push({ name, code, bytes: Buffer.concat([
|
|
87
|
+
u32(name.length), utf16(name), Buffer.from(code + type), ...(type === "blob" ? [u32(bytes.length)] : []), bytes,
|
|
88
|
+
]) });
|
|
89
|
+
add(".", "vSrn", "long", u32(1));
|
|
90
|
+
add(".", "icvl", "type", Buffer.from("icnv"));
|
|
91
|
+
add(".", "bwsp", "blob", finderPlist({ ShowStatusBar: false, ShowToolbar: false, ShowTabView: false,
|
|
92
|
+
ShowPathbar: false, ShowSidebar: false, ContainerShowSidebar: false, PreviewPaneVisibility: false,
|
|
93
|
+
SidebarWidth: 0, WindowBounds: `{{100, 100}, {${window[0]}, ${window[1]}}}` }));
|
|
94
|
+
add(".", "icvp", "blob", finderPlist({ viewOptionsVersion: 1, backgroundType: alias ? 2 : 0,
|
|
95
|
+
backgroundColorRed: 1, backgroundColorGreen: 1, backgroundColorBlue: 1,
|
|
96
|
+
gridOffsetX: 0, gridOffsetY: 0, gridSpacing: 100, arrangeBy: "none", showIconPreview: true,
|
|
97
|
+
showItemInfo: false, labelOnBottom: true, textSize: 16, iconSize,
|
|
98
|
+
scrollPositionX: 0, scrollPositionY: 0, ...(alias ? { backgroundImageAlias: alias } : {}) }));
|
|
99
|
+
for (const [name, point] of [[appName, appPosition], ["Applications", applicationsPosition]]) {
|
|
100
|
+
add(name, "Iloc", "blob", Buffer.concat([u32(point[0]), u32(point[1]), u32(0xffffffff), u32(0xffff0000)]));
|
|
101
|
+
}
|
|
102
|
+
entries.sort((a, b) => a.name.toLowerCase() < b.name.toLowerCase() ? -1
|
|
103
|
+
: a.name.toLowerCase() > b.name.toLowerCase() ? 1 : a.code < b.code ? -1 : 1);
|
|
104
|
+
const leafData = Buffer.concat([u32(0), u32(entries.length), ...entries.map((e) => e.bytes)]);
|
|
105
|
+
const pagePower = Math.max(12, Math.ceil(Math.log2(leafData.length))), pageSize = 2 ** pagePower;
|
|
106
|
+
if (pagePower > 20) throw new RangeError("DMG layout too large");
|
|
107
|
+
// Larger leaf starts at its own aligned boundary; unused buddies remain free.
|
|
108
|
+
const leafOffset = pageSize, file = Buffer.alloc(leafOffset + pageSize + 4);
|
|
109
|
+
Buffer.concat([u32(1), Buffer.from("Bud1"), u32(2048), u32(2048), u32(2048),
|
|
110
|
+
Buffer.from("0000100c000000870000200b00000000", "hex")]).copy(file);
|
|
111
|
+
const root = Buffer.alloc(2048); let pos = 0;
|
|
112
|
+
const write = (bytes) => { bytes.copy(root, pos); pos += bytes.length; };
|
|
113
|
+
write(u32(3)); write(u32(0)); write(u32(2048 | 11)); write(u32(32 | 5)); write(u32(leafOffset | pagePower));
|
|
114
|
+
pos = 1032; write(u32(1)); write(Buffer.from([4])); write(Buffer.from("DSDB")); write(u32(1));
|
|
115
|
+
for (let power = 0; power < 32; power++) {
|
|
116
|
+
const free = power >= 6 && power <= 10 || power >= 12 && power !== pagePower && power <= 30;
|
|
117
|
+
write(u32(free ? 1 : 0)); if (free) write(u32(2 ** power));
|
|
118
|
+
}
|
|
119
|
+
root.copy(file, 2052);
|
|
120
|
+
Buffer.concat([u32(2), u32(0), u32(entries.length), u32(1), u32(pageSize)]).copy(file, 36);
|
|
121
|
+
leafData.copy(file, leafOffset + 4);
|
|
122
|
+
return file;
|
|
123
|
+
}
|