|
| 1 | +import * as semver from 'semver'; |
| 2 | +import { spawn } from 'node:child_process'; |
| 3 | +import { Readable } from 'node:stream'; |
| 4 | + |
| 5 | +export const DEFAULT_EXEC_TIMEOUT = 10 * 60 * 1000; // ms |
| 6 | +export const SIM_RUNTIME_NAME = 'com.apple.CoreSimulator.SimRuntime.'; |
| 7 | + |
| 8 | +/** |
| 9 | + * "Normalize" the version, since iOS uses 'major.minor' but the runtimes can |
| 10 | + * be 'major.minor.patch' |
| 11 | + * |
| 12 | + * @param version - the string version |
| 13 | + * @return The version in 'major.minor' form |
| 14 | + * @throws {Error} If the version not parseable by the `semver` package |
| 15 | + */ |
| 16 | +export function normalizeVersion (version: string): string { |
| 17 | + const semverVersion = semver.coerce(version); |
| 18 | + if (!semverVersion) { |
| 19 | + throw new Error(`Unable to parse version '${version}'`); |
| 20 | + } |
| 21 | + return `${semverVersion.major}.${semverVersion.minor}`; |
| 22 | +} |
| 23 | + |
| 24 | +/** |
| 25 | + * @returns The xcrun binary name |
| 26 | + */ |
| 27 | +export function getXcrunBinary (): string { |
| 28 | + return process.env.XCRUN_BINARY || 'xcrun'; |
| 29 | +} |
| 30 | + |
| 31 | +/** |
| 32 | + * Generate a UUID v4 |
| 33 | + * |
| 34 | + * @returns Promise resolving to UUID string |
| 35 | + */ |
| 36 | +export async function uuidV4 (): Promise<string> { |
| 37 | + const uuidLib = await import('uuid'); |
| 38 | + return uuidLib.v4(); |
| 39 | +} |
| 40 | + |
| 41 | +/** |
| 42 | + * Convert plist-style output to JSON using plutil |
| 43 | + * |
| 44 | + * @param plistInput - The plist-style string to convert |
| 45 | + * @return Promise resolving to parsed JSON object |
| 46 | + * @throws {Error} If plutil fails to convert the input |
| 47 | + */ |
| 48 | +export async function convertPlistToJson (plistInput: string): Promise<any> { |
| 49 | + const plutilProcess = spawn('plutil', ['-convert', 'json', '-o', '-', '-']); |
| 50 | + let jsonOutput = ''; |
| 51 | + plutilProcess.stdout.on('data', (chunk) => { |
| 52 | + jsonOutput += chunk.toString(); |
| 53 | + }); |
| 54 | + const inputStream = Readable.from([plistInput]); |
| 55 | + inputStream.pipe(plutilProcess.stdin); |
| 56 | + try { |
| 57 | + await new Promise<void>((resolve, reject) => { |
| 58 | + inputStream.once('error', reject); |
| 59 | + plutilProcess.once('exit', (code, signal) => { |
| 60 | + inputStream.unpipe(plutilProcess.stdin); |
| 61 | + if (code === 0) { |
| 62 | + resolve(); |
| 63 | + } else { |
| 64 | + reject(new Error(`plutil exited with code ${code}, signal ${signal}`)); |
| 65 | + } |
| 66 | + }); |
| 67 | + plutilProcess.once('error', (e) => { |
| 68 | + inputStream.unpipe(plutilProcess.stdin); |
| 69 | + reject(e); |
| 70 | + }); |
| 71 | + }); |
| 72 | + } catch (err) { |
| 73 | + plutilProcess.kill(9); |
| 74 | + throw new Error(`Failed to convert plist to JSON: ${err instanceof Error ? err.message : String(err)}`); |
| 75 | + } finally { |
| 76 | + plutilProcess.removeAllListeners(); |
| 77 | + inputStream.removeAllListeners(); |
| 78 | + } |
| 79 | + return JSON.parse(jsonOutput); |
| 80 | +} |
| 81 | + |
0 commit comments