| 1 | /* |
| 2 | * @description Cross-platform macOS flat package builder for MeshAgent installers. |
| 3 | * Creates a XAR-based distribution package instead of the legacy bundle .mpkg |
| 4 | * format that macOS Sequoia/Tahoe rejects. |
| 5 | */ |
| 6 | |
| 7 | 'use strict'; |
| 8 | |
| 9 | const crypto = require('crypto'); |
| 10 | const fs = require('fs'); |
| 11 | const fsp = fs.promises; |
| 12 | const os = require('os'); |
| 13 | const path = require('path'); |
| 14 | const zlib = require('zlib'); |
| 15 | const childProcess = require('child_process'); |
| 16 | const { promisify } = require('util'); |
| 17 | |
| 18 | const deflate = promisify(zlib.deflate); |
| 19 | const execFile = promisify(childProcess.execFile); |
| 20 | |
| 21 | const LAUNCH_DAEMON_PLIST = `<?xml version="1.0" encoding="UTF-8"?> |
| 22 | <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
| 23 | <plist version="1.0"> |
| 24 | <dict> |
| 25 | <key>Label</key> |
| 26 | <string>###SERVICENAME###</string> |
| 27 | <key>ProgramArguments</key> |
| 28 | <array> |
| 29 | <string>/usr/local/mesh_services/###COMPANYNAME###/###SERVICENAME###/###EXECUTABLENAME###</string> |
| 30 | </array> |
| 31 | <key>WorkingDirectory</key> |
| 32 | <string>/usr/local/mesh_services/###COMPANYNAME###/###SERVICENAME###/</string> |
| 33 | <key>RunAtLoad</key> |
| 34 | <true/> |
| 35 | <key>KeepAlive</key> |
| 36 | <true/> |
| 37 | <key>ThrottleInterval</key> |
| 38 | <integer>1</integer> |
| 39 | </dict> |
| 40 | </plist> |
| 41 | `; |
| 42 | |
| 43 | const LAUNCH_AGENT_PLIST = `<?xml version="1.0" encoding="UTF-8"?> |
| 44 | <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
| 45 | <plist version="1.0"> |
| 46 | <dict> |
| 47 | <key>Label</key> |
| 48 | <string>###SERVICENAME###-launchagent</string> |
| 49 | <key>ProgramArguments</key> |
| 50 | <array> |
| 51 | <string>/usr/local/mesh_services/###COMPANYNAME###/###SERVICENAME###/###EXECUTABLENAME###</string> |
| 52 | <string>-kvmagent</string> |
| 53 | </array> |
| 54 | <key>LimitLoadToSessionType</key> |
| 55 | <array> |
| 56 | <string>LoginWindow</string> |
| 57 | <string>Aqua</string> |
| 58 | </array> |
| 59 | <key>WorkingDirectory</key> |
| 60 | <string>/usr/local/mesh_services/###COMPANYNAME###/###SERVICENAME###/</string> |
| 61 | <key>RunAtLoad</key> |
| 62 | <true/> |
| 63 | <key>KeepAlive</key> |
| 64 | <true/> |
| 65 | </dict> |
| 66 | </plist> |
| 67 | `; |
| 68 | |
| 69 | const POSTINSTALL = `#!/bin/bash |
| 70 | set -e |
| 71 | |
| 72 | SERVICENAME="###SERVICENAME###" |
| 73 | COMPANYNAME="###COMPANYNAME###" |
| 74 | EXECUTABLENAME="###EXECUTABLENAME###" |
| 75 | INSTALLDIR="/usr/local/mesh_services/\${COMPANYNAME}/\${SERVICENAME}" |
| 76 | |
| 77 | chown -R root:wheel "/usr/local/mesh_services/\${COMPANYNAME}" || true |
| 78 | chown root:wheel "\${INSTALLDIR}/\${EXECUTABLENAME}" "\${INSTALLDIR}/\${EXECUTABLENAME}.msh" |
| 79 | chown root:wheel "/Library/LaunchDaemons/\${SERVICENAME}.plist" "/Library/LaunchAgents/\${SERVICENAME}-launchagent.plist" |
| 80 | |
| 81 | chmod 755 "\${INSTALLDIR}" "\${INSTALLDIR}/\${EXECUTABLENAME}" |
| 82 | chmod 644 "\${INSTALLDIR}/\${EXECUTABLENAME}.msh" "/Library/LaunchDaemons/\${SERVICENAME}.plist" "/Library/LaunchAgents/\${SERVICENAME}-launchagent.plist" |
| 83 | |
| 84 | /bin/launchctl bootout system "/Library/LaunchDaemons/\${SERVICENAME}.plist" >/dev/null 2>&1 || true |
| 85 | /bin/launchctl bootstrap system "/Library/LaunchDaemons/\${SERVICENAME}.plist" >/dev/null 2>&1 || /bin/launchctl load "/Library/LaunchDaemons/\${SERVICENAME}.plist" |
| 86 | |
| 87 | CONSOLE_USER=$(stat -f '%Su' /dev/console 2>/dev/null || true) |
| 88 | CONSOLE_UID=$(id -u "\${CONSOLE_USER}" 2>/dev/null || true) |
| 89 | if [ -n "\${CONSOLE_UID}" ] && [ "\${CONSOLE_UID}" != "0" ]; then |
| 90 | /bin/launchctl bootout "gui/\${CONSOLE_UID}" "/Library/LaunchAgents/\${SERVICENAME}-launchagent.plist" >/dev/null 2>&1 || true |
| 91 | /bin/launchctl bootstrap "gui/\${CONSOLE_UID}" "/Library/LaunchAgents/\${SERVICENAME}-launchagent.plist" >/dev/null 2>&1 || true |
| 92 | fi |
| 93 | `; |
| 94 | |
| 95 | const UNINSTALL = `#!/bin/bash |
| 96 | |
| 97 | echo "Stopping ###SERVICENAME###..." |
| 98 | sudo /bin/launchctl bootout system "/Library/LaunchDaemons/###SERVICENAME###.plist" &> /dev/null || sudo /bin/launchctl unload "/Library/LaunchDaemons/###SERVICENAME###.plist" &> /dev/null |
| 99 | sudo pkill -9 "###SERVICENAME###" &> /dev/null || true |
| 100 | CONSOLE_USER=$(stat -f '%Su' /dev/console 2>/dev/null || true) |
| 101 | CONSOLE_UID=$(id -u "\${CONSOLE_USER}" 2>/dev/null || true) |
| 102 | if [ -n "\${CONSOLE_UID}" ] && [ "\${CONSOLE_UID}" != "0" ]; then |
| 103 | sudo /bin/launchctl bootout "gui/\${CONSOLE_UID}" "/Library/LaunchAgents/###SERVICENAME###-launchagent.plist" &> /dev/null || true |
| 104 | fi |
| 105 | sudo /bin/launchctl unload "/Library/LaunchDaemons/meshagentDiagnostic_periodicStart.plist" &> /dev/null |
| 106 | sudo /bin/launchctl unload "/Library/LaunchDaemons/meshagentDiagnostic.plist" &> /dev/null |
| 107 | sudo rm "/Library/LaunchDaemons/meshagentDiagnostic_periodicStart.plist" &> /dev/null |
| 108 | sudo rm "/Library/LaunchDaemons/meshagentDiagnostic.plist" &> /dev/null |
| 109 | |
| 110 | echo "Resetting TCC permissions for ###SERVICENAME###..." |
| 111 | BUNDLE_ID=$(mdls -name kMDItemCFBundleIdentifier -raw "/usr/local/mesh_services/###COMPANYNAME###/###SERVICENAME###/###EXECUTABLENAME###" 2>/dev/null || true) |
| 112 | if [ -n "\${BUNDLE_ID}" ] && [ "\${BUNDLE_ID}" != "(null)" ]; then |
| 113 | sudo tccutil reset All "\${BUNDLE_ID}" &> /dev/null || true |
| 114 | fi |
| 115 | sudo tccutil reset All "###SERVICENAME###" &> /dev/null || true |
| 116 | |
| 117 | sudo rm "/usr/local/mesh_services/###COMPANYNAME###/###SERVICENAME###/###EXECUTABLENAME###" &> /dev/null |
| 118 | sudo rm "/usr/local/mesh_services/###COMPANYNAME###/###SERVICENAME###/###EXECUTABLENAME###.msh" &> /dev/null |
| 119 | sudo rm "/usr/local/mesh_services/###COMPANYNAME###/###SERVICENAME###/###EXECUTABLENAME###.db" &> /dev/null |
| 120 | sudo rm "/usr/local/mesh_services/meshagentDiagnostic/meshagentDiagnostic" &> /dev/null |
| 121 | sudo rm "/Library/LaunchDaemons/###SERVICENAME###.plist" &> /dev/null |
| 122 | sudo rm "/Library/LaunchAgents/###SERVICENAME###-launchagent.plist" &> /dev/null |
| 123 | echo "###SERVICENAME### was uninstalled." |
| 124 | `; |
| 125 | |
| 126 | function replaceTokens(str, tokens) { |
| 127 | return str.split('###SERVICENAME###').join(tokens.serviceName) |
| 128 | .split('###COMPANYNAME###').join(tokens.companyName) |
| 129 | .split('###EXECUTABLENAME###').join(tokens.executableName); |
| 130 | } |
| 131 | |
| 132 | function xmlEscape(str) { |
| 133 | return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, '''); |
| 134 | } |
| 135 | |
| 136 | function pkgIdentifierSegment(str) { |
| 137 | return String(str).toLowerCase().replace(/[^a-z0-9.-]/g, '-').replace(/^-+|-+$/g, '') || 'meshagent'; |
| 138 | } |
| 139 | |
| 140 | async function chmodIfExists(file, mode) { |
| 141 | try { await fsp.chmod(file, mode); } catch (ex) { } |
| 142 | } |
| 143 | |
| 144 | async function walk(dir) { |
| 145 | const entries = await fsp.readdir(dir, { withFileTypes: true }); |
| 146 | let files = 0, bytes = 0; |
| 147 | for (const entry of entries) { |
| 148 | const p = path.join(dir, entry.name); |
| 149 | if (entry.isDirectory()) { |
| 150 | const r = await walk(p); |
| 151 | files += r.files; |
| 152 | bytes += r.bytes; |
| 153 | } else if (entry.isFile()) { |
| 154 | const s = await fsp.stat(p); |
| 155 | files++; |
| 156 | bytes += s.size; |
| 157 | } |
| 158 | } |
| 159 | return { files, bytes }; |
| 160 | } |
| 161 | |
| 162 | function pad4(buffer) { |
| 163 | const pad = (4 - (buffer.length % 4)) % 4; |
| 164 | return (pad === 0) ? buffer : Buffer.concat([buffer, Buffer.alloc(pad)]); |
| 165 | } |
| 166 | |
| 167 | function octal(value, width) { |
| 168 | const max = Math.pow(8, width) - 1; |
| 169 | const n = Math.max(0, Math.min(Number(value) || 0, max)); |
| 170 | return Math.floor(n).toString(8).padStart(width, '0').slice(-width); |
| 171 | } |
| 172 | |
| 173 | async function collectPayloadEntries(root, relativePath) { |
| 174 | const fullPath = path.join(root, relativePath); |
| 175 | const stat = await fsp.stat(fullPath); |
| 176 | const entries = []; |
| 177 | if (relativePath !== '') { |
| 178 | entries.push({ name: relativePath.split(path.sep).join('/'), stat: stat, data: stat.isFile() ? await fsp.readFile(fullPath) : null }); |
| 179 | } |
| 180 | if (stat.isDirectory()) { |
| 181 | const names = (await fsp.readdir(fullPath)).sort(); |
| 182 | for (const name of names) { entries.push.apply(entries, await collectPayloadEntries(root, path.join(relativePath, name))); } |
| 183 | } |
| 184 | return entries; |
| 185 | } |
| 186 | |
| 187 | function cpioOdcRecord(name, mode, data, ino, mtime) { |
| 188 | data = data || Buffer.alloc(0); |
| 189 | const nameBuffer = Buffer.from(name + '\0', 'utf8'); |
| 190 | const header = [ |
| 191 | '070707', |
| 192 | octal(0, 6), // dev |
| 193 | octal(ino, 6), |
| 194 | octal(mode, 6), |
| 195 | octal(0, 6), // uid |
| 196 | octal(0, 6), // gid |
| 197 | octal(1, 6), // nlink |
| 198 | octal(0, 6), // rdev |
| 199 | octal(mtime || Math.floor(Date.now() / 1000), 11), |
| 200 | octal(nameBuffer.length, 6), |
| 201 | octal(data.length, 11) |
| 202 | ].join(''); |
| 203 | return Buffer.concat([Buffer.from(header, 'ascii'), nameBuffer, data]); |
| 204 | } |
| 205 | |
| 206 | async function createPayload(payloadRoot, targetFile) { |
| 207 | const entries = await collectPayloadEntries(payloadRoot, ''); |
| 208 | const records = []; |
| 209 | let ino = 1; |
| 210 | for (const entry of entries) { |
| 211 | records.push(cpioOdcRecord(entry.name, entry.stat.mode, entry.data, ino++, Math.floor(entry.stat.mtimeMs / 1000))); |
| 212 | } |
| 213 | records.push(cpioOdcRecord('TRAILER!!!', 0, Buffer.alloc(0), ino)); |
| 214 | await fsp.writeFile(targetFile, zlib.gzipSync(Buffer.concat(records))); |
| 215 | } |
| 216 | |
| 217 | async function createBom(payloadRoot, targetFile) { |
| 218 | try { |
| 219 | await execFile('mkbom', [payloadRoot, targetFile], { timeout: 30000 }); |
| 220 | } catch (ex) { |
| 221 | // Linux/Windows hosts can still build the package archive without a |
| 222 | // third-party BOM dependency. macOS hosts use the native mkbom tool |
| 223 | // above so local validation keeps the richer bill of materials. |
| 224 | await fsp.writeFile(targetFile, Buffer.alloc(0)); |
| 225 | } |
| 226 | } |
| 227 | |
| 228 | async function collectXarEntry(filePath, name, id) { |
| 229 | const stat = await fsp.stat(filePath); |
| 230 | const entry = { |
| 231 | id: id, |
| 232 | name: name, |
| 233 | type: stat.isDirectory() ? 'directory' : 'file', |
| 234 | mode: stat.mode, |
| 235 | uid: stat.uid, |
| 236 | gid: stat.gid, |
| 237 | atime: stat.atime, |
| 238 | mtime: stat.mtime, |
| 239 | ctime: stat.ctime |
| 240 | }; |
| 241 | if (stat.isFile()) { |
| 242 | entry.data = await fsp.readFile(filePath); |
| 243 | } else if (stat.isDirectory()) { |
| 244 | const names = (await fsp.readdir(filePath)).sort(); |
| 245 | entry.children = []; |
| 246 | for (const childName of names) { |
| 247 | entry.children.push(await collectXarEntry(path.join(filePath, childName), childName, ++collectXarEntry.nextId)); |
| 248 | } |
| 249 | } |
| 250 | return entry; |
| 251 | } |
| 252 | |
| 253 | function xarDate(d) { |
| 254 | return d.toISOString(); |
| 255 | } |
| 256 | |
| 257 | function xarFileXml(entry, depth, heapParts) { |
| 258 | const indent = ' '.repeat(depth); |
| 259 | let xml = indent + '<file id="' + entry.id + '">\n' |
| 260 | + indent + ' <name>' + xmlEscape(entry.name) + '</name>\n' |
| 261 | + indent + ' <type>' + entry.type + '</type>\n' |
| 262 | + indent + ' <mode>' + entry.mode.toString(8) + '</mode>\n' |
| 263 | + indent + ' <uid>' + entry.uid + '</uid>\n' |
| 264 | + indent + ' <gid>' + entry.gid + '</gid>\n' |
| 265 | + indent + ' <atime>' + xarDate(entry.atime) + '</atime>\n' |
| 266 | + indent + ' <mtime>' + xarDate(entry.mtime) + '</mtime>\n' |
| 267 | + indent + ' <ctime>' + xarDate(entry.ctime) + '</ctime>\n'; |
| 268 | if (entry.type == 'file') { |
| 269 | const offset = 20 + heapParts.reduce(function (total, part) { return total + part.length; }, 0); |
| 270 | const sum = crypto.createHash('sha1').update(entry.data).digest('hex'); |
| 271 | heapParts.push(entry.data); |
| 272 | xml += indent + ' <data>\n' |
| 273 | + indent + ' <archived-checksum style="sha1">' + sum + '</archived-checksum>\n' |
| 274 | + indent + ' <extracted-checksum style="sha1">' + sum + '</extracted-checksum>\n' |
| 275 | + indent + ' <offset>' + offset + '</offset>\n' |
| 276 | + indent + ' <encoding style="application/octet-stream"/>\n' |
| 277 | + indent + ' <size>' + entry.data.length + '</size>\n' |
| 278 | + indent + ' <length>' + entry.data.length + '</length>\n' |
| 279 | + indent + ' </data>\n'; |
| 280 | } else { |
| 281 | for (const child of entry.children) { xml += xarFileXml(child, depth + 1, heapParts); } |
| 282 | } |
| 283 | return xml + indent + '</file>\n'; |
| 284 | } |
| 285 | |
| 286 | async function createXarPackage(paths) { |
| 287 | collectXarEntry.nextId = 0; |
| 288 | const entries = []; |
| 289 | for (const p of paths) { entries.push(await collectXarEntry(p, path.basename(p), ++collectXarEntry.nextId)); } |
| 290 | |
| 291 | const heapParts = []; |
| 292 | let toc = '<?xml version="1.0" encoding="UTF-8"?>\n<xar>\n <toc>\n' |
| 293 | + ' <checksum style="sha1">\n <size>20</size>\n <offset>0</offset>\n </checksum>\n' |
| 294 | + ' <creation-time>' + (new Date()).toISOString() + '</creation-time>\n'; |
| 295 | for (const entry of entries) { toc += xarFileXml(entry, 2, heapParts); } |
| 296 | toc += ' </toc>\n</xar>'; |
| 297 | |
| 298 | const tocBuffer = Buffer.from(toc, 'utf8'); |
| 299 | const compressedToc = await deflate(tocBuffer); |
| 300 | const tocChecksum = crypto.createHash('sha1').update(compressedToc).digest(); |
| 301 | const header = Buffer.alloc(28); |
| 302 | header.writeUInt32BE(0x78617221, 0); // xar! |
| 303 | header.writeUInt16BE(28, 4); |
| 304 | header.writeUInt16BE(1, 6); |
| 305 | header.writeBigUInt64BE(BigInt(compressedToc.length), 8); |
| 306 | header.writeBigUInt64BE(BigInt(tocBuffer.length), 16); |
| 307 | header.writeUInt32BE(1, 24); // sha1 |
| 308 | return Buffer.concat([header, compressedToc, tocChecksum].concat(heapParts)); |
| 309 | } |
| 310 | |
| 311 | async function createMacOSInstaller(opts) { |
| 312 | const tmpRoot = await fsp.mkdtemp(path.join(os.tmpdir(), 'meshcentral-macos-pkg-')); |
| 313 | try { |
| 314 | const payloadRoot = path.join(tmpRoot, 'payload'); |
| 315 | const scriptsRoot = path.join(tmpRoot, 'scripts'); |
| 316 | const basePkg = path.join(tmpRoot, 'internal.pkg'); |
| 317 | const resourcesDir = path.join(tmpRoot, 'Resources'); |
| 318 | const installDir = path.join(payloadRoot, 'usr', 'local', 'mesh_services', opts.companyName, opts.serviceName); |
| 319 | const launchDaemons = path.join(payloadRoot, 'Library', 'LaunchDaemons'); |
| 320 | const launchAgents = path.join(payloadRoot, 'Library', 'LaunchAgents'); |
| 321 | const tokens = { serviceName: opts.serviceName, companyName: opts.companyName, executableName: opts.executableName }; |
| 322 | |
| 323 | await fsp.mkdir(installDir, { recursive: true }); |
| 324 | await fsp.mkdir(launchDaemons, { recursive: true }); |
| 325 | await fsp.mkdir(launchAgents, { recursive: true }); |
| 326 | await fsp.mkdir(basePkg, { recursive: true }); |
| 327 | await fsp.mkdir(scriptsRoot, { recursive: true }); |
| 328 | await fsp.mkdir(resourcesDir, { recursive: true }); |
| 329 | |
| 330 | await fsp.copyFile(opts.agentPath, path.join(installDir, opts.executableName)); |
| 331 | await fsp.writeFile(path.join(installDir, opts.executableName + '.msh'), opts.meshSettings); |
| 332 | await fsp.writeFile(path.join(launchDaemons, opts.serviceName + '.plist'), replaceTokens(LAUNCH_DAEMON_PLIST, tokens)); |
| 333 | await fsp.writeFile(path.join(launchAgents, opts.serviceName + '-launchagent.plist'), replaceTokens(LAUNCH_AGENT_PLIST, tokens)); |
| 334 | await fsp.writeFile(path.join(scriptsRoot, 'postinstall'), replaceTokens(POSTINSTALL, tokens)); |
| 335 | |
| 336 | if (opts.backgroundPath) { |
| 337 | await fsp.copyFile(opts.backgroundPath, path.join(resourcesDir, 'background')); |
| 338 | } else { |
| 339 | const backgroundPath = path.join(__dirname, 'agents', 'macosinstallerbackground.png'); |
| 340 | await fsp.copyFile(backgroundPath, path.join(resourcesDir, 'background')); |
| 341 | } |
| 342 | |
| 343 | await chmodIfExists(path.join(installDir, opts.executableName), 0o755); |
| 344 | await chmodIfExists(path.join(scriptsRoot, 'postinstall'), 0o755); |
| 345 | await chmodIfExists(path.join(installDir, opts.executableName + '.msh'), 0o644); |
| 346 | await chmodIfExists(path.join(launchDaemons, opts.serviceName + '.plist'), 0o644); |
| 347 | await chmodIfExists(path.join(launchAgents, opts.serviceName + '-launchagent.plist'), 0o644); |
| 348 | |
| 349 | const payloadStats = await walk(payloadRoot); |
| 350 | const installKBytes = Math.ceil(payloadStats.bytes / 1000); |
| 351 | await createPayload(payloadRoot, path.join(basePkg, 'Payload')); |
| 352 | await createPayload(scriptsRoot, path.join(basePkg, 'Scripts')); |
| 353 | await createBom(payloadRoot, path.join(basePkg, 'Bom')); |
| 354 | |
| 355 | const packageInfo = '<pkg-info format-version="2" identifier="com.meshcentral.' + xmlEscape(pkgIdentifierSegment(opts.serviceName)) + '.pkg" version="1.0" install-location="/" relocatable="false" auth="root">\n' |
| 356 | + ' <payload installKBytes="' + installKBytes + '" numberOfFiles="' + payloadStats.files + '"/>\n' |
| 357 | + ' <scripts>\n' |
| 358 | + ' <postinstall file="./postinstall"/>\n' |
| 359 | + ' </scripts>\n' |
| 360 | + '</pkg-info>\n'; |
| 361 | await fsp.writeFile(path.join(basePkg, 'PackageInfo'), packageInfo); |
| 362 | |
| 363 | const welcome = 'Welcome to the MeshCentral agent for MacOS\n\nThis installer will install the mesh agent for "' + opts.meshName + '" and allow the administrator to remotely monitor and control this computer over the internet. For more information, go to https://meshcentral.com.\n\nThis software is provided under Apache 2.0 license.\n'; |
| 364 | const distribution = '<?xml version="1.0" encoding="utf-8"?>\n' |
| 365 | + '<installer-script minSpecVersion="1.000000">\n' |
| 366 | + ' <title>' + xmlEscape(opts.displayName) + '</title>\n' |
| 367 | + ' <options customize="always" allow-external-scripts="no" rootVolumeOnly="true"/>\n' |
| 368 | + ' <background file="background" alignment="topleft" scaling="tofit"/>\n' |
| 369 | + ' <welcome language="en-US" mime-type="text/plain"><![CDATA[' + welcome.split(']]>').join(']]]]><![CDATA[>') + ']]></welcome>\n' |
| 370 | + ' <choices-outline>\n' |
| 371 | + ' <line choice="choice65"/>\n' |
| 372 | + ' </choices-outline>\n' |
| 373 | + ' <choice id="choice65" title="' + xmlEscape(opts.displayName) + '">\n' |
| 374 | + ' <pkg-ref id="internal.pkg"/>\n' |
| 375 | + ' </choice>\n' |
| 376 | + ' <pkg-ref id="internal.pkg" installKBytes="' + installKBytes + '" version="1.0" auth="Root">#internal.pkg</pkg-ref>\n' |
| 377 | + ' <options hostArchitectures="arm64,x86_64"/>\n' |
| 378 | + '</installer-script>\n'; |
| 379 | await fsp.writeFile(path.join(tmpRoot, 'Distribution'), distribution); |
| 380 | |
| 381 | const pkgBuffer = await createXarPackage([basePkg, resourcesDir, path.join(tmpRoot, 'Distribution')]); |
| 382 | return { |
| 383 | pkg: pkgBuffer, |
| 384 | uninstall: replaceTokens(UNINSTALL, tokens) |
| 385 | }; |
| 386 | } finally { |
| 387 | await fsp.rm(tmpRoot, { recursive: true, force: true }); |
| 388 | } |
| 389 | } |
| 390 | |
| 391 | module.exports = { createMacOSInstaller }; |