@ricsam/r5d-macos-vm 0.0.0 → 0.0.55

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.
@@ -0,0 +1,144 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { spawn } from "node:child_process";
5
+ import {
6
+ findPackageRoot,
7
+ findPackageVersion,
8
+ readNativeArtifactManifest
9
+ } from "./artifact-manifest.mjs";
10
+ import { readHostInformation, validateHostInformation } from "./host.mjs";
11
+ import { defaultCacheRoot, ensureNativeRuntime, pruneObsoleteNativeRuntimes } from "./native-runtime.mjs";
12
+ function defaultModulePath() {
13
+ if (typeof __filename === "string") return __filename;
14
+ return fileURLToPath(import.meta.url);
15
+ }
16
+ const HELP = `Usage: r5d-macos-vm <command> [options]
17
+
18
+ Commands:
19
+ doctor [--json] Check host and native virtualization support
20
+ create [name] [...] Download macOS from Apple and create a VM
21
+ list [--json] List locally managed VMs
22
+ status [name] [--json] Show VM state
23
+ start [name] [--json] Start a VM in its window
24
+ run [name] [--json] Alias for start
25
+ stop [name] [--force] Stop a VM
26
+ delete <name> --yes [--purge] Move a VM to Trash, or permanently purge it
27
+ cache prune Remove restore images and old native runners
28
+ --version Show the package version
29
+
30
+ `;
31
+ function findBootstrapDirectory(modulePath) {
32
+ const packageRoot = findPackageRoot(modulePath);
33
+ const candidate = packageRoot ? path.join(packageRoot, "bootstrap") : null;
34
+ if (candidate && fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) return candidate;
35
+ throw new Error("The guest bootstrap assets are missing. Reinstall @ricsam/r5d-macos-vm.");
36
+ }
37
+ const NAMED_COMMANDS = /* @__PURE__ */ new Set(["create", "run", "start", "status", "stop", "delete", "delete-check"]);
38
+ function validateVmName(name) {
39
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.-]{0,62}$/.test(name)) {
40
+ throw new Error("VM names must start with a letter or number and contain at most 63 letters, numbers, dots, underscores, or hyphens.");
41
+ }
42
+ }
43
+ function findCommandIndex(args) {
44
+ for (let index = 0; index < args.length; index += 1) {
45
+ if (args[index] === "--json") continue;
46
+ if (args[index] === "--state-root") {
47
+ index += 1;
48
+ continue;
49
+ }
50
+ return index;
51
+ }
52
+ return -1;
53
+ }
54
+ function normalizeNativeArguments(args, bootstrapDir) {
55
+ const normalized = [...args];
56
+ const commandIndex = findCommandIndex(normalized);
57
+ if (commandIndex < 0) return normalized;
58
+ const command = normalized[commandIndex];
59
+ if (NAMED_COMMANDS.has(command)) {
60
+ const nameOptionIndex = normalized.indexOf("--name");
61
+ if (nameOptionIndex >= 0) {
62
+ const name = normalized[nameOptionIndex + 1];
63
+ if (!name || name.startsWith("-")) throw new Error("Missing value for --name.");
64
+ validateVmName(name);
65
+ } else {
66
+ let positionalIndex = commandIndex + 1;
67
+ while (normalized[positionalIndex] === "--json" || normalized[positionalIndex] === "--state-root") {
68
+ if (normalized[positionalIndex] === "--state-root") positionalIndex += 2;
69
+ else positionalIndex += 1;
70
+ }
71
+ const positionalName = normalized[positionalIndex];
72
+ if (positionalName && !positionalName.startsWith("-")) {
73
+ validateVmName(positionalName);
74
+ normalized.splice(positionalIndex, 1, "--name", positionalName);
75
+ }
76
+ }
77
+ }
78
+ if (command === "create" && !normalized.includes("--bootstrap-dir")) {
79
+ normalized.push("--bootstrap-dir", bootstrapDir);
80
+ }
81
+ return normalized;
82
+ }
83
+ const invokeNativeProcess = (executable, args) => new Promise((resolve, reject) => {
84
+ const child = spawn(executable, [...args], { stdio: "inherit", shell: false });
85
+ const forwardSignal = (signal) => child.kill(signal);
86
+ const onSigint = () => forwardSignal("SIGINT");
87
+ const onSigterm = () => forwardSignal("SIGTERM");
88
+ process.once("SIGINT", onSigint);
89
+ process.once("SIGTERM", onSigterm);
90
+ child.once("error", reject);
91
+ child.once("close", (code, signal) => {
92
+ process.off("SIGINT", onSigint);
93
+ process.off("SIGTERM", onSigterm);
94
+ if (signal) resolve(128 + (signal === "SIGINT" ? 2 : 15));
95
+ else resolve(code ?? 1);
96
+ });
97
+ });
98
+ function resolveManifestPath(modulePath, explicitPath) {
99
+ if (explicitPath) return path.resolve(explicitPath);
100
+ const packageRoot = findPackageRoot(modulePath);
101
+ const candidate = packageRoot ? path.join(packageRoot, "native-artifact.json") : null;
102
+ if (!candidate || !fs.existsSync(candidate)) {
103
+ throw new Error("The native artifact manifest is missing. Reinstall @ricsam/r5d-macos-vm.");
104
+ }
105
+ return candidate;
106
+ }
107
+ async function runCli(args, dependencies = {}) {
108
+ const modulePath = dependencies.modulePath ?? defaultModulePath();
109
+ const stdout = dependencies.stdout ?? process.stdout;
110
+ const packageVersion = dependencies.packageVersion ?? findPackageVersion(modulePath);
111
+ if (args.length === 0 || args[0] === "--help" || args[0] === "-h" || args[0] === "help") {
112
+ stdout.write(HELP);
113
+ return 0;
114
+ }
115
+ if (args[0] === "--version" || args[0] === "-v" || args[0] === "version") {
116
+ stdout.write(`r5d-macos-vm ${packageVersion}
117
+ `);
118
+ return 0;
119
+ }
120
+ validateHostInformation(dependencies.hostInformation ?? await readHostInformation());
121
+ const manifest = readNativeArtifactManifest(resolveManifestPath(modulePath, dependencies.manifestPath), packageVersion);
122
+ const executable = await (dependencies.ensureRuntime ?? ensureNativeRuntime)(manifest, dependencies.runtime);
123
+ const bootstrapDir = dependencies.bootstrapDir ?? findBootstrapDirectory(modulePath);
124
+ const nativeArgs = normalizeNativeArguments(args, bootstrapDir);
125
+ const exitCode = await (dependencies.invokeNative ?? invokeNativeProcess)(executable, nativeArgs);
126
+ const commandIndex = findCommandIndex(nativeArgs);
127
+ if (exitCode === 0 && commandIndex >= 0 && nativeArgs[commandIndex] === "cache" && nativeArgs[commandIndex + 1] === "prune") {
128
+ await (dependencies.pruneRuntimes ?? pruneObsoleteNativeRuntimes)(
129
+ executable,
130
+ dependencies.runtime?.cacheRoot ?? defaultCacheRoot()
131
+ );
132
+ }
133
+ return exitCode;
134
+ }
135
+ function getHelpText() {
136
+ return HELP;
137
+ }
138
+ export {
139
+ getHelpText,
140
+ invokeNativeProcess,
141
+ normalizeNativeArguments,
142
+ runCli,
143
+ validateVmName
144
+ };
@@ -0,0 +1,37 @@
1
+ import { spawn } from "node:child_process";
2
+ function runSwVers() {
3
+ return new Promise((resolve, reject) => {
4
+ const child = spawn("/usr/bin/sw_vers", ["-productVersion"], {
5
+ stdio: ["ignore", "pipe", "pipe"],
6
+ env: { ...process.env, LANG: "C", LC_ALL: "C" }
7
+ });
8
+ let stdout = "";
9
+ let stderr = "";
10
+ child.stdout.setEncoding("utf8").on("data", (chunk) => stdout += chunk);
11
+ child.stderr.setEncoding("utf8").on("data", (chunk) => stderr += chunk);
12
+ child.once("error", reject);
13
+ child.once("close", (code) => {
14
+ if (code === 0) resolve(stdout.trim());
15
+ else reject(new Error(`sw_vers failed (${code ?? "signal"}): ${stderr.trim()}`));
16
+ });
17
+ });
18
+ }
19
+ async function readHostInformation() {
20
+ return {
21
+ platform: process.platform,
22
+ architecture: process.arch,
23
+ macOSVersion: process.platform === "darwin" ? await runSwVers() : "unknown"
24
+ };
25
+ }
26
+ function validateHostInformation(host) {
27
+ if (host.platform !== "darwin") throw new Error("r5d-macos-vm requires macOS.");
28
+ if (host.architecture !== "arm64") throw new Error("r5d-macos-vm requires an Apple silicon Mac (arm64).");
29
+ const match = /^(\d+)(?:\.(\d+))?/.exec(host.macOSVersion);
30
+ if (!match || Number(match[1]) < 14) {
31
+ throw new Error(`r5d-macos-vm requires macOS 14 or newer (found ${host.macOSVersion}).`);
32
+ }
33
+ }
34
+ export {
35
+ readHostInformation,
36
+ validateHostInformation
37
+ };
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from "./cli.mjs";
3
+ runCli(process.argv.slice(2)).then((exitCode) => {
4
+ process.exitCode = exitCode;
5
+ }).catch((error) => {
6
+ process.stderr.write(`r5d-macos-vm: ${error instanceof Error ? error.message : String(error)}
7
+ `);
8
+ process.exitCode = 1;
9
+ });
@@ -0,0 +1,421 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { spawn } from "node:child_process";
6
+ const LOCK_OWNER_FILE = "owner.json";
7
+ const TEMPORARY_ARCHIVE_SUFFIX = ".download";
8
+ async function ensurePrivateDirectory(directory, description) {
9
+ try {
10
+ await fs.promises.mkdir(directory, { recursive: true, mode: 448 });
11
+ } catch (error) {
12
+ if (error.code !== "EEXIST") throw error;
13
+ }
14
+ const stat = await fs.promises.lstat(directory);
15
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
16
+ throw new Error(`Refusing to use ${description} because it is not a regular directory: ${directory}`);
17
+ }
18
+ const currentUid = process.getuid?.();
19
+ if (currentUid !== void 0 && stat.uid !== currentUid) {
20
+ throw new Error(`Refusing to use ${description} because it is not owned by the current user: ${directory}`);
21
+ }
22
+ await fs.promises.chmod(directory, 448);
23
+ }
24
+ function defaultCacheRoot() {
25
+ return path.join(os.homedir(), "Library", "Caches", "dev.r5d.macos-vm");
26
+ }
27
+ function runtimeDirectory(cacheRoot, manifest) {
28
+ return path.join(cacheRoot, "runtimes", manifest.packageVersion);
29
+ }
30
+ function nativeExecutablePath(runtimeDir, manifest) {
31
+ return path.join(runtimeDir, manifest.appName, "Contents", "MacOS", manifest.executableName);
32
+ }
33
+ async function pruneObsoleteNativeRuntimes(currentExecutable, cacheRoot = defaultCacheRoot()) {
34
+ const runtimesRoot = path.resolve(cacheRoot, "runtimes");
35
+ const currentRuntime = path.resolve(currentExecutable, "../../../../");
36
+ if (path.dirname(currentRuntime) !== runtimesRoot) {
37
+ throw new Error("Refusing to prune native runtimes because the active runtime is outside the managed cache.");
38
+ }
39
+ await ensurePrivateDirectory(path.resolve(cacheRoot), "native runtime cache");
40
+ await ensurePrivateDirectory(runtimesRoot, "native runtimes directory");
41
+ let entries;
42
+ try {
43
+ entries = await fs.promises.readdir(runtimesRoot, { withFileTypes: true });
44
+ } catch (error) {
45
+ if (error.code === "ENOENT") return [];
46
+ throw error;
47
+ }
48
+ const removed = [];
49
+ const runtimeNamePattern = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
50
+ for (const entry of entries) {
51
+ if (!entry.isDirectory() || entry.isSymbolicLink() || !runtimeNamePattern.test(entry.name)) continue;
52
+ const candidate = path.resolve(runtimesRoot, entry.name);
53
+ if (candidate === currentRuntime || path.dirname(candidate) !== runtimesRoot) continue;
54
+ const quarantine = path.join(runtimesRoot, `.prune-${process.pid}-${crypto.randomUUID()}`);
55
+ try {
56
+ await fs.promises.rename(candidate, quarantine);
57
+ } catch (error) {
58
+ if (error.code === "ENOENT") continue;
59
+ throw error;
60
+ }
61
+ await fs.promises.rm(quarantine, { recursive: true, force: true });
62
+ removed.push(entry.name);
63
+ }
64
+ return removed;
65
+ }
66
+ function delay(milliseconds) {
67
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
68
+ }
69
+ async function removeStaleLock(lockPath, staleLockMs) {
70
+ let stat;
71
+ try {
72
+ stat = await fs.promises.stat(lockPath);
73
+ } catch (error) {
74
+ if (error.code === "ENOENT") return true;
75
+ throw error;
76
+ }
77
+ if (Date.now() - stat.mtimeMs < staleLockMs) return false;
78
+ let ownerPid = null;
79
+ try {
80
+ const owner = JSON.parse(await fs.promises.readFile(path.join(lockPath, LOCK_OWNER_FILE), "utf8"));
81
+ if (Number.isSafeInteger(owner.pid) && owner.pid > 0) ownerPid = owner.pid;
82
+ } catch {
83
+ }
84
+ if (ownerPid !== null) {
85
+ try {
86
+ process.kill(ownerPid, 0);
87
+ return false;
88
+ } catch (error) {
89
+ if (error.code === "EPERM") return false;
90
+ }
91
+ }
92
+ const stalePath = `${lockPath}.stale-${process.pid}-${crypto.randomUUID()}`;
93
+ try {
94
+ await fs.promises.rename(lockPath, stalePath);
95
+ } catch (error) {
96
+ if (error.code === "ENOENT") return true;
97
+ return false;
98
+ }
99
+ await fs.promises.rm(stalePath, { recursive: true, force: true });
100
+ return true;
101
+ }
102
+ async function withDirectoryLock(lockPath, action, options) {
103
+ const startedAt = Date.now();
104
+ await ensurePrivateDirectory(path.dirname(lockPath), "native runtime lock directory");
105
+ while (true) {
106
+ try {
107
+ await fs.promises.mkdir(lockPath, { mode: 448 });
108
+ await fs.promises.chmod(lockPath, 448);
109
+ await fs.promises.writeFile(
110
+ path.join(lockPath, LOCK_OWNER_FILE),
111
+ `${JSON.stringify({ pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString() })}
112
+ `,
113
+ { mode: 384 }
114
+ );
115
+ break;
116
+ } catch (error) {
117
+ if (error.code !== "EEXIST") throw error;
118
+ if (await removeStaleLock(lockPath, options.staleLockMs)) continue;
119
+ if (Date.now() - startedAt >= options.timeoutMs) {
120
+ throw new Error(`Timed out waiting for native runtime installation lock at ${lockPath}.`);
121
+ }
122
+ await delay(options.pollMs);
123
+ }
124
+ }
125
+ try {
126
+ return await action();
127
+ } finally {
128
+ await fs.promises.rm(lockPath, { recursive: true, force: true });
129
+ }
130
+ }
131
+ const runCommand = (executable, args) => new Promise((resolve, reject) => {
132
+ const child = spawn(executable, [...args], {
133
+ stdio: ["ignore", "pipe", "pipe"],
134
+ env: { ...process.env, LANG: "C", LC_ALL: "C" },
135
+ shell: false
136
+ });
137
+ let stdout = "";
138
+ let stderr = "";
139
+ child.stdout.setEncoding("utf8").on("data", (chunk) => stdout += chunk);
140
+ child.stderr.setEncoding("utf8").on("data", (chunk) => stderr += chunk);
141
+ child.once("error", reject);
142
+ child.once("close", (code) => resolve({ exitCode: code ?? 1, stdout, stderr }));
143
+ });
144
+ async function requireSuccessfulCommand(runner, executable, args, description) {
145
+ const result = await runner(executable, args);
146
+ if (result.exitCode !== 0) {
147
+ const detail = `${result.stdout}
148
+ ${result.stderr}`.trim();
149
+ throw new Error(`${description} failed${detail ? `: ${detail}` : "."}`);
150
+ }
151
+ return result;
152
+ }
153
+ async function downloadNativeArtifact(manifest, destination) {
154
+ const temporaryDestination = `${destination}${TEMPORARY_ARCHIVE_SUFFIX}`;
155
+ await fs.promises.rm(temporaryDestination, { force: true });
156
+ const controller = new AbortController();
157
+ const timeout = setTimeout(() => controller.abort(), 5 * 6e4);
158
+ let response;
159
+ try {
160
+ response = await fetch(manifest.url, {
161
+ signal: controller.signal,
162
+ redirect: "follow",
163
+ headers: { "user-agent": `@ricsam/r5d-macos-vm/${manifest.packageVersion}` }
164
+ });
165
+ } catch (error) {
166
+ clearTimeout(timeout);
167
+ throw new Error(`Unable to download the native runtime: ${error instanceof Error ? error.message : String(error)}`);
168
+ }
169
+ try {
170
+ if (!response.ok || !response.body) throw new Error(`Native runtime download returned HTTP ${response.status}.`);
171
+ const contentLength = response.headers.get("content-length");
172
+ if (contentLength !== null && Number(contentLength) !== manifest.byteLength) {
173
+ throw new Error(`Native runtime download length ${contentLength} does not match expected ${manifest.byteLength}.`);
174
+ }
175
+ const file = await fs.promises.open(temporaryDestination, "wx", 384);
176
+ const hash = crypto.createHash("sha256");
177
+ let byteLength = 0;
178
+ try {
179
+ for await (const value of response.body) {
180
+ const chunk = Buffer.from(value);
181
+ byteLength += chunk.byteLength;
182
+ if (byteLength > manifest.byteLength) throw new Error("Native runtime download exceeded its signed byte length.");
183
+ hash.update(chunk);
184
+ await file.write(chunk);
185
+ }
186
+ } finally {
187
+ await file.close();
188
+ }
189
+ if (byteLength !== manifest.byteLength) {
190
+ throw new Error(`Native runtime download contained ${byteLength} bytes; expected ${manifest.byteLength}.`);
191
+ }
192
+ const digest = hash.digest("hex");
193
+ if (!crypto.timingSafeEqual(Buffer.from(digest, "hex"), Buffer.from(manifest.sha256, "hex"))) {
194
+ throw new Error("Native runtime checksum verification failed.");
195
+ }
196
+ await fs.promises.rename(temporaryDestination, destination);
197
+ } finally {
198
+ clearTimeout(timeout);
199
+ await fs.promises.rm(temporaryDestination, { force: true });
200
+ }
201
+ }
202
+ function decodePackedVersion(value) {
203
+ const major = value >>> 16;
204
+ const minor = value >>> 8 & 255;
205
+ const patch = value & 255;
206
+ return patch === 0 ? `${major}.${minor}` : `${major}.${minor}.${patch}`;
207
+ }
208
+ function inspectMachOExecutable(filePath) {
209
+ const data = fs.readFileSync(filePath);
210
+ if (data.byteLength < 32 || data.readUInt32LE(0) !== 4277009103) {
211
+ throw new Error("Native runtime executable is not a thin 64-bit Mach-O file.");
212
+ }
213
+ if (data.readInt32LE(4) !== 16777228) throw new Error("Native runtime executable is not arm64.");
214
+ if (data.readUInt32LE(12) !== 2) throw new Error("Native runtime Mach-O is not an executable.");
215
+ const commandCount = data.readUInt32LE(16);
216
+ const commandBytes = data.readUInt32LE(20);
217
+ if (32 + commandBytes > data.byteLength) throw new Error("Native runtime Mach-O load commands are truncated.");
218
+ let offset = 32;
219
+ let minimumMacOSVersion = null;
220
+ for (let index = 0; index < commandCount; index += 1) {
221
+ if (offset + 8 > data.byteLength) throw new Error("Native runtime Mach-O load command is truncated.");
222
+ const command = data.readUInt32LE(offset);
223
+ const commandSize = data.readUInt32LE(offset + 4);
224
+ if (commandSize < 8 || offset + commandSize > data.byteLength) throw new Error("Native runtime Mach-O load command is invalid.");
225
+ if (command === 50) {
226
+ if (commandSize < 24) throw new Error("Native runtime LC_BUILD_VERSION command is invalid.");
227
+ if (data.readUInt32LE(offset + 8) !== 1) throw new Error("Native runtime was not built for macOS.");
228
+ minimumMacOSVersion = decodePackedVersion(data.readUInt32LE(offset + 12));
229
+ } else if (command === 36 && minimumMacOSVersion === null) {
230
+ if (commandSize < 16) throw new Error("Native runtime LC_VERSION_MIN_MACOSX command is invalid.");
231
+ minimumMacOSVersion = decodePackedVersion(data.readUInt32LE(offset + 8));
232
+ }
233
+ offset += commandSize;
234
+ }
235
+ if (minimumMacOSVersion === null) throw new Error("Native runtime does not declare a minimum macOS version.");
236
+ return { architecture: "arm64", minimumMacOSVersion };
237
+ }
238
+ function normalizeVersion(value) {
239
+ const parts = value.trim().split(".").map(Number);
240
+ while (parts.length > 2 && parts.at(-1) === 0) parts.pop();
241
+ return parts.join(".");
242
+ }
243
+ function requireRegularFile(filePath, description) {
244
+ const stat = fs.lstatSync(filePath);
245
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`${description} is not a regular file.`);
246
+ }
247
+ function parseSigningValue(output, key) {
248
+ const line = output.split(/\r?\n/).find((candidate) => candidate.startsWith(`${key}=`));
249
+ return line ? line.slice(key.length + 1).trim() : null;
250
+ }
251
+ function codeSignatureUsesHardenedRuntime(output) {
252
+ const codeDirectoryLine = output.split(/\r?\n/).find((line) => line.startsWith("CodeDirectory "));
253
+ if (!codeDirectoryLine) return false;
254
+ const flags = /\bflags=0x[0-9a-f]+\(([^)]*)\)/i.exec(codeDirectoryLine)?.[1];
255
+ return flags?.split(",").some((flag) => flag.trim() === "runtime") ?? false;
256
+ }
257
+ async function verifyNativeApp(appPath, manifest, runner = runCommand) {
258
+ const appStat = fs.lstatSync(appPath);
259
+ if (!appStat.isDirectory() || appStat.isSymbolicLink()) throw new Error("Native runtime app is not a regular bundle directory.");
260
+ const infoPlistPath = path.join(appPath, "Contents", "Info.plist");
261
+ const executablePath = path.join(appPath, "Contents", "MacOS", manifest.executableName);
262
+ requireRegularFile(infoPlistPath, "Native runtime Info.plist");
263
+ requireRegularFile(executablePath, "Native runtime executable");
264
+ const machO = inspectMachOExecutable(executablePath);
265
+ if (machO.architecture !== manifest.architecture) throw new Error("Native runtime architecture does not match its manifest.");
266
+ if (normalizeVersion(machO.minimumMacOSVersion) !== normalizeVersion(manifest.minimumMacOSVersion)) {
267
+ throw new Error(`Native runtime minimum macOS version ${machO.minimumMacOSVersion} does not match ${manifest.minimumMacOSVersion}.`);
268
+ }
269
+ const identifier = await requireSuccessfulCommand(
270
+ runner,
271
+ "/usr/bin/plutil",
272
+ ["-extract", "CFBundleIdentifier", "raw", "-o", "-", infoPlistPath],
273
+ "Reading native runtime bundle identifier"
274
+ );
275
+ if (identifier.stdout.trim() !== manifest.bundleIdentifier) throw new Error("Native runtime bundle identifier is invalid.");
276
+ const minimumVersion = await requireSuccessfulCommand(
277
+ runner,
278
+ "/usr/bin/plutil",
279
+ ["-extract", "LSMinimumSystemVersion", "raw", "-o", "-", infoPlistPath],
280
+ "Reading native runtime deployment target"
281
+ );
282
+ if (normalizeVersion(minimumVersion.stdout) !== normalizeVersion(manifest.minimumMacOSVersion)) {
283
+ throw new Error("Native runtime Info.plist deployment target is invalid.");
284
+ }
285
+ await requireSuccessfulCommand(
286
+ runner,
287
+ "/usr/bin/codesign",
288
+ ["--verify", "--deep", "--strict", "--verbose=2", appPath],
289
+ "Native runtime code-signature verification"
290
+ );
291
+ const signature = await requireSuccessfulCommand(
292
+ runner,
293
+ "/usr/bin/codesign",
294
+ ["--display", "--verbose=4", appPath],
295
+ "Reading native runtime code signature"
296
+ );
297
+ const signatureOutput = `${signature.stdout}
298
+ ${signature.stderr}`;
299
+ if (parseSigningValue(signatureOutput, "Identifier") !== manifest.bundleIdentifier) {
300
+ throw new Error("Native runtime code-signing identifier is invalid.");
301
+ }
302
+ if (parseSigningValue(signatureOutput, "TeamIdentifier") !== manifest.teamIdentifier) {
303
+ throw new Error("Native runtime Apple Team ID is invalid.");
304
+ }
305
+ if (!codeSignatureUsesHardenedRuntime(signatureOutput)) {
306
+ throw new Error("Native runtime is not signed with the hardened runtime.");
307
+ }
308
+ if (parseSigningValue(signatureOutput, "Notarization Ticket") !== "stapled") {
309
+ throw new Error("Native runtime does not contain a stapled notarization ticket.");
310
+ }
311
+ const entitlements = await requireSuccessfulCommand(
312
+ runner,
313
+ "/usr/bin/codesign",
314
+ ["--display", "--entitlements", ":-", appPath],
315
+ "Reading native runtime entitlements"
316
+ );
317
+ const entitlementOutput = `${entitlements.stdout}
318
+ ${entitlements.stderr}`;
319
+ const entitlementKeys = [...entitlementOutput.matchAll(/<key>([^<]+)<\/key>/g)].map((match) => match[1]);
320
+ if (entitlementKeys.length !== 1 || entitlementKeys[0] !== "com.apple.security.virtualization") {
321
+ throw new Error("Native runtime contains entitlements other than com.apple.security.virtualization.");
322
+ }
323
+ if (!/<key>com\.apple\.security\.virtualization<\/key>\s*<true\s*\/>/s.test(entitlementOutput)) {
324
+ throw new Error("Native runtime is missing the virtualization entitlement.");
325
+ }
326
+ const gatekeeper = await requireSuccessfulCommand(
327
+ runner,
328
+ "/usr/sbin/spctl",
329
+ ["--assess", "--type", "execute", "--verbose=4", appPath],
330
+ "Native runtime Gatekeeper assessment"
331
+ );
332
+ const gatekeeperOutput = `${gatekeeper.stdout}
333
+ ${gatekeeper.stderr}`;
334
+ if (!/source=Notarized Developer ID/.test(gatekeeperOutput)) {
335
+ throw new Error("Native runtime does not have an accepted Apple notarization assessment.");
336
+ }
337
+ }
338
+ async function installRuntime(runtimeDir, manifest, dependencies) {
339
+ const runtimesRoot = path.dirname(runtimeDir);
340
+ await ensurePrivateDirectory(runtimesRoot, "native runtimes directory");
341
+ const workDir = await fs.promises.mkdtemp(path.join(runtimesRoot, ".install-"));
342
+ try {
343
+ const archivePath = path.join(workDir, manifest.archiveName);
344
+ await dependencies.download(manifest, archivePath);
345
+ const extractDir = path.join(workDir, "extract");
346
+ await fs.promises.mkdir(extractDir, { mode: 448 });
347
+ await requireSuccessfulCommand(
348
+ dependencies.commandRunner,
349
+ "/usr/bin/ditto",
350
+ ["-x", "-k", "--noqtn", archivePath, extractDir],
351
+ "Extracting native runtime"
352
+ );
353
+ const entries = await fs.promises.readdir(extractDir);
354
+ if (entries.length !== 1 || entries[0] !== manifest.appName) {
355
+ throw new Error(`Native runtime archive must contain only ${manifest.appName}.`);
356
+ }
357
+ const extractedApp = path.join(extractDir, manifest.appName);
358
+ await verifyNativeApp(extractedApp, manifest, dependencies.commandRunner);
359
+ const readyDir = path.join(workDir, "ready");
360
+ await fs.promises.mkdir(readyDir, { mode: 448 });
361
+ await fs.promises.rename(extractedApp, path.join(readyDir, manifest.appName));
362
+ await fs.promises.writeFile(
363
+ path.join(readyDir, "runtime.json"),
364
+ `${JSON.stringify({ packageVersion: manifest.packageVersion, sha256: manifest.sha256 }, null, 2)}
365
+ `,
366
+ { mode: 384 }
367
+ );
368
+ await fs.promises.rename(readyDir, runtimeDir);
369
+ } finally {
370
+ await fs.promises.rm(workDir, { recursive: true, force: true });
371
+ }
372
+ }
373
+ async function cachedRuntimeIsValid(runtimeDir, manifest, runner) {
374
+ try {
375
+ const marker = JSON.parse(await fs.promises.readFile(path.join(runtimeDir, "runtime.json"), "utf8"));
376
+ if (marker.packageVersion !== manifest.packageVersion || marker.sha256 !== manifest.sha256) return false;
377
+ await verifyNativeApp(path.join(runtimeDir, manifest.appName), manifest, runner);
378
+ return true;
379
+ } catch {
380
+ return false;
381
+ }
382
+ }
383
+ async function ensureNativeRuntime(manifest, dependencies = {}) {
384
+ const cacheRoot = dependencies.cacheRoot ?? defaultCacheRoot();
385
+ const commandRunner = dependencies.commandRunner ?? runCommand;
386
+ const download = dependencies.download ?? downloadNativeArtifact;
387
+ const destination = runtimeDirectory(cacheRoot, manifest);
388
+ await ensurePrivateDirectory(cacheRoot, "native runtime cache");
389
+ await ensurePrivateDirectory(path.dirname(destination), "native runtimes directory");
390
+ if (await cachedRuntimeIsValid(destination, manifest, commandRunner)) {
391
+ return nativeExecutablePath(destination, manifest);
392
+ }
393
+ const lockPath = path.join(cacheRoot, "locks", `${manifest.packageVersion}.lock`);
394
+ return withDirectoryLock(
395
+ lockPath,
396
+ async () => {
397
+ if (!await cachedRuntimeIsValid(destination, manifest, commandRunner)) {
398
+ await fs.promises.rm(destination, { recursive: true, force: true });
399
+ await installRuntime(destination, manifest, { commandRunner, download });
400
+ }
401
+ return nativeExecutablePath(destination, manifest);
402
+ },
403
+ {
404
+ pollMs: dependencies.lockPollMs ?? 100,
405
+ timeoutMs: dependencies.lockTimeoutMs ?? 5 * 6e4,
406
+ staleLockMs: dependencies.staleLockMs ?? 10 * 6e4
407
+ }
408
+ );
409
+ }
410
+ export {
411
+ codeSignatureUsesHardenedRuntime,
412
+ defaultCacheRoot,
413
+ downloadNativeArtifact,
414
+ ensureNativeRuntime,
415
+ inspectMachOExecutable,
416
+ nativeExecutablePath,
417
+ pruneObsoleteNativeRuntimes,
418
+ runCommand,
419
+ runtimeDirectory,
420
+ verifyNativeApp
421
+ };
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "@ricsam/r5d-macos-vm",
3
+ "version": "0.0.55",
4
+ "type": "module"
5
+ }
@@ -0,0 +1,33 @@
1
+ export declare const MACOS_VM_PACKAGE_NAME = "@ricsam/r5d-macos-vm";
2
+ export declare const NATIVE_ARCHIVE_NAME = "R5DMacOSVM.app.zip";
3
+ export declare const NATIVE_APP_NAME = "R5DMacOSVM.app";
4
+ export declare const NATIVE_EXECUTABLE_NAME = "R5DMacOSVM";
5
+ export declare const NATIVE_BUNDLE_IDENTIFIER = "dev.r5d.macos-vm";
6
+ export declare const NATIVE_MINIMUM_MACOS_VERSION = "14.0";
7
+ export declare const NATIVE_ARTIFACT_HOST = "downloads.r5d.dev";
8
+ export type NativeArtifactManifest = {
9
+ schemaVersion: 1;
10
+ packageVersion: string;
11
+ url: string;
12
+ sha256: string;
13
+ byteLength: number;
14
+ archiveName: typeof NATIVE_ARCHIVE_NAME;
15
+ appName: typeof NATIVE_APP_NAME;
16
+ executableName: typeof NATIVE_EXECUTABLE_NAME;
17
+ bundleIdentifier: typeof NATIVE_BUNDLE_IDENTIFIER;
18
+ teamIdentifier: string;
19
+ architecture: "arm64";
20
+ minimumMacOSVersion: typeof NATIVE_MINIMUM_MACOS_VERSION;
21
+ };
22
+ export declare function expectedArtifactUrl(version: string): string;
23
+ /**
24
+ * Identify Apple restore media and machine-specific VM state that must never be
25
+ * shipped in either the npm package or the separately hosted native archive.
26
+ */
27
+ export declare function isForbiddenVirtualMachineArtifactPath(filePath: string): boolean;
28
+ export declare function findForbiddenVirtualMachineArtifactPaths(paths: readonly string[]): string[];
29
+ export declare function parseNativeArtifactManifest(value: unknown, expectedPackageVersion: string): NativeArtifactManifest;
30
+ export declare function readNativeArtifactManifest(filePath: string, expectedPackageVersion: string): NativeArtifactManifest;
31
+ export declare function findFileUpward(startPath: string, fileName: string): string | null;
32
+ export declare function findPackageRoot(startPath: string): string | null;
33
+ export declare function findPackageVersion(startPath: string): string;
@@ -0,0 +1,23 @@
1
+ import { type NativeArtifactManifest } from "./artifact-manifest";
2
+ import { type HostInformation } from "./host";
3
+ import { type RuntimeDependencies } from "./native-runtime";
4
+ type CliDependencies = {
5
+ modulePath?: string;
6
+ packageVersion?: string;
7
+ manifestPath?: string;
8
+ bootstrapDir?: string;
9
+ hostInformation?: HostInformation;
10
+ runtime?: RuntimeDependencies;
11
+ ensureRuntime?: (manifest: NativeArtifactManifest, dependencies?: RuntimeDependencies) => Promise<string>;
12
+ invokeNative?: (executable: string, args: readonly string[]) => Promise<number>;
13
+ pruneRuntimes?: (currentExecutable: string, cacheRoot?: string) => Promise<string[]>;
14
+ stdout?: {
15
+ write: (value: string) => unknown;
16
+ };
17
+ };
18
+ export declare function validateVmName(name: string): void;
19
+ export declare function normalizeNativeArguments(args: readonly string[], bootstrapDir: string): string[];
20
+ export declare const invokeNativeProcess: (executable: string, args: readonly string[]) => Promise<number>;
21
+ export declare function runCli(args: readonly string[], dependencies?: CliDependencies): Promise<number>;
22
+ export declare function getHelpText(): string;
23
+ export {};