@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.
Files changed (81) hide show
  1. package/README.md +62 -0
  2. package/addon-command.mjs +53 -34
  3. package/apple-profile-renew.mjs +30 -12
  4. package/artifact-storage.mjs +47 -10
  5. package/background-process.mjs +18 -0
  6. package/build-release.mjs +1 -1
  7. package/cache-command.mjs +4 -3
  8. package/cargo-contract.mjs +1 -1
  9. package/cargo-guard.mjs +4 -3
  10. package/cargo-registry-readback.mjs +51 -0
  11. package/cargo-target.mjs +3 -1
  12. package/cli/right-release.mjs +26 -0
  13. package/codesign-environment.mjs +26 -2
  14. package/codesign-shim.mjs +2 -1
  15. package/committed-source.mjs +3 -3
  16. package/development.mjs +7 -7
  17. package/direct-bootstrap.mjs +19 -9
  18. package/dmg-build.mjs +117 -0
  19. package/dmg-layout.mjs +123 -0
  20. package/github-release.mjs +74 -37
  21. package/heavy-command.mjs +3 -2
  22. package/install-app.mjs +358 -0
  23. package/inventory-check.mjs +980 -0
  24. package/macos-signing-session.mjs +3 -0
  25. package/managed-cargo.mjs +23 -19
  26. package/managed-host-secrets.mjs +30 -0
  27. package/managed-roots.mjs +39 -0
  28. package/model-promote.mjs +31 -37
  29. package/notary-auth.mjs +4 -0
  30. package/osascript-guard.mjs +24 -0
  31. package/ownership-approval-command.mjs +15 -4
  32. package/ownership-approval.mjs +114 -49
  33. package/ownership-availability.mjs +25 -0
  34. package/ownership-catalog-config.json +18 -0
  35. package/ownership-catalog.mjs +94 -0
  36. package/ownership-command.mjs +20 -10
  37. package/ownership-declaration.json +15 -0
  38. package/ownership-default-policy.json +1 -11
  39. package/ownership-evidence.mjs +10 -8
  40. package/ownership-git-inventory.mjs +29 -1
  41. package/ownership-helper-paths.json +2 -2
  42. package/ownership-hooks.mjs +107 -0
  43. package/ownership-internal-owners.json +44 -0
  44. package/ownership-key-authority.mjs +112 -7
  45. package/ownership-key-command.mjs +76 -19
  46. package/ownership-key-trust.mjs +97 -9
  47. package/ownership-lockfiles.mjs +311 -0
  48. package/ownership-report-command.mjs +12 -3
  49. package/ownership-report.mjs +20 -17
  50. package/ownership-repositories.json +19 -0
  51. package/ownership-scan-worker.mjs +96 -11
  52. package/ownership-source.mjs +39 -0
  53. package/ownership-staged.mjs +193 -0
  54. package/ownership-verdict.mjs +74 -0
  55. package/ownership.mjs +215 -107
  56. package/package.json +1 -1
  57. package/protected-credentials.mjs +111 -0
  58. package/prune-r2.mjs +4 -3
  59. package/pub-upload.mjs +3 -85
  60. package/publication-artifacts.mjs +108 -0
  61. package/publication-gate.mjs +28 -7
  62. package/publish-cargo.mjs +47 -16
  63. package/publish-swift.mjs +46 -17
  64. package/publish-update.mjs +61 -39
  65. package/r2-verified-upload.mjs +41 -0
  66. package/registry-parity.mjs +5 -3
  67. package/release-state.mjs +1 -0
  68. package/release-token.mjs +2 -0
  69. package/rightkit-drift.mjs +3 -2
  70. package/rightkit-ownership.json +2028 -139
  71. package/rightkit-signing-agent.mjs +5 -3
  72. package/rightkit-sync.mjs +6 -2
  73. package/rightkit-versions.json +215 -103
  74. package/runtime-artifact-manifest.mjs +20 -6
  75. package/sign-updater.mjs +5 -0
  76. package/signing-agent-client.mjs +2 -1
  77. package/signing-agent-install.mjs +3 -2
  78. package/standalone-clone-verify.mjs +3 -2
  79. package/tauri-bundle-marker.mjs +3 -2
  80. package/upload-large.mjs +36 -82
  81. package/verifier.mjs +5 -1
@@ -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, PATH: [bin, ...(env.PATH ?? "").split(path.delimiter).filter((entry) => entry !== bin)].join(path.delimiter) };
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
- if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
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.
@@ -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 managedRoot = result.RIGHTKIT_MANAGED_ROOT ?? result.RIGHTKIT_EXTERNAL_ROOT
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 = managedRoot && path.join(managedRoot, "broker.sock");
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 = path.join(path.dirname(path.dirname(path.dirname(path.dirname(client)))), "broker.sock");
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
- if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
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
  }
@@ -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 validation = validatePowerShellBootstrap(readFileSync(scriptPath, "utf8"), { product });
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: sha256File(scriptPath),
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
- r2.put(plan.bucket, plan.versionedKey, plan.scriptPath);
571
- r2.get(plan.bucket, plan.versionedKey, verificationPath);
572
- if (sha256File(verificationPath) !== plan.sha256) throw new Error("immutable bootstrap remote verification failed");
573
- r2.put(plan.bucket, plan.stableKey, plan.scriptPath);
574
- r2.get(plan.bucket, plan.stableKey, verificationPath);
575
- if (sha256File(verificationPath) !== plan.sha256) throw new Error("stable bootstrap remote verification failed");
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
+ }