@rage-against-the-pixel/unity-cli 3.0.2 → 4.0.1

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 (51) hide show
  1. package/README.md +86 -224
  2. package/dist/android-sdk.js +8 -1
  3. package/dist/android-sdk.js.map +1 -1
  4. package/dist/cli.js +180 -505
  5. package/dist/cli.js.map +1 -1
  6. package/dist/failure-markers.d.ts +17 -0
  7. package/dist/failure-markers.js +46 -0
  8. package/dist/failure-markers.js.map +1 -0
  9. package/dist/github-actions-ci.d.ts +1 -0
  10. package/dist/github-actions-ci.js +17 -3
  11. package/dist/github-actions-ci.js.map +1 -1
  12. package/dist/index.d.ts +2 -2
  13. package/dist/index.js +2 -2
  14. package/dist/index.js.map +1 -1
  15. package/dist/license-client.d.ts +119 -2
  16. package/dist/license-client.js +737 -56
  17. package/dist/license-client.js.map +1 -1
  18. package/dist/logger-provider.d.ts +2 -0
  19. package/dist/logger-provider.js +3 -0
  20. package/dist/logger-provider.js.map +1 -1
  21. package/dist/logging.d.ts +5 -0
  22. package/dist/logging.js +47 -0
  23. package/dist/logging.js.map +1 -1
  24. package/dist/process-tracker.d.ts +71 -0
  25. package/dist/process-tracker.js +213 -0
  26. package/dist/process-tracker.js.map +1 -0
  27. package/dist/unity-cli.d.ts +97 -0
  28. package/dist/unity-cli.js +591 -0
  29. package/dist/unity-cli.js.map +1 -0
  30. package/dist/unity-editor.d.ts +25 -16
  31. package/dist/unity-editor.js +204 -107
  32. package/dist/unity-editor.js.map +1 -1
  33. package/dist/unity-installer.d.ts +139 -0
  34. package/dist/unity-installer.js +1018 -0
  35. package/dist/unity-installer.js.map +1 -0
  36. package/dist/unity-logging.d.ts +16 -1
  37. package/dist/unity-logging.js +55 -3
  38. package/dist/unity-logging.js.map +1 -1
  39. package/dist/unity-project.d.ts +15 -0
  40. package/dist/unity-project.js +80 -0
  41. package/dist/unity-project.js.map +1 -1
  42. package/dist/upm-cli.d.ts +8 -8
  43. package/dist/upm-cli.js +8 -8
  44. package/dist/upm-cli.js.map +1 -1
  45. package/dist/utilities.d.ts +27 -7
  46. package/dist/utilities.js +119 -63
  47. package/dist/utilities.js.map +1 -1
  48. package/package.json +10 -11
  49. package/dist/unity-hub.d.ts +0 -140
  50. package/dist/unity-hub.js +0 -1425
  51. package/dist/unity-hub.js.map +0 -1
@@ -0,0 +1,1018 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.UnityInstaller = void 0;
37
+ exports.resolveEditorExecutable = resolveEditorExecutable;
38
+ exports.moveMacInstalledItem = moveMacInstalledItem;
39
+ exports.getExecutableArchitectures = getExecutableArchitectures;
40
+ const fs = __importStar(require("fs"));
41
+ const os = __importStar(require("os"));
42
+ const path = __importStar(require("path"));
43
+ const crypto_1 = require("crypto");
44
+ const child_process_1 = require("child_process");
45
+ const logging_1 = require("./logging");
46
+ const unity_editor_1 = require("./unity-editor");
47
+ const unity_version_1 = require("./unity-version");
48
+ const unity_cli_1 = require("./unity-cli");
49
+ const process_tracker_1 = require("./process-tracker");
50
+ const utilities_1 = require("./utilities");
51
+ const unity_releases_api_1 = require("@rage-against-the-pixel/unity-releases-api");
52
+ /** Exit codes the official CLI documents as retryable: network, install locked and timeout. */
53
+ const RETRYABLE_EXIT_CODES = [7, 9, 14];
54
+ /**
55
+ * Packages the Unity Linux Editor needs that Ubuntu runners do not ship.
56
+ * Some of them are only in jammy, so its archive is added as a source.
57
+ */
58
+ const LINUX_EDITOR_DEPENDENCIES_SCRIPT = `#!/bin/sh
59
+ set -e
60
+ # an Ubuntu jammy source only works on Ubuntu; a Debian or other apt host would break its own apt-get update
61
+ if grep -qs '^ID=ubuntu$' /etc/os-release; then
62
+ if [ ! -f /etc/apt/sources.list.d/jammy.list ]; then
63
+ echo "deb https://archive.ubuntu.com/ubuntu jammy main universe" > /etc/apt/sources.list.d/jammy.list
64
+ fi
65
+ # jammy only supplies packages the host release lacks; it never replaces the host's own packages
66
+ printf 'Package: *\\nPin: release n=jammy\\nPin-Priority: 100\\n' > /etc/apt/preferences.d/unity-cli-jammy
67
+ fi
68
+ export DEBIAN_FRONTEND=noninteractive
69
+ # a stalled mirror must not hold the job: apt retries slow requests, and each step has a hard limit
70
+ APT_OPTS="-o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 -o Acquire::Retries=3 -o DPkg::Lock::Timeout=120"
71
+ timeout 300 apt-get $APT_OPTS update
72
+ # without pulseaudio, FMOD logs audio device errors on 2019 and 2020 editors; it pulls in about 38 MB,
73
+ # which the runners' Azure mirror has taken over 10 minutes to serve
74
+ timeout 1200 apt-get $APT_OPTS install -y --no-install-recommends xvfb libgtk2.0-0 libglu1-mesa libgconf-2-4 libncurses5 pulseaudio
75
+ `;
76
+ /** ffmpeg is only for video capture and import, so it is opt-in with UNITY_CLI_LINUX_FFMPEG=true. */
77
+ const LINUX_FFMPEG_SCRIPT = `timeout 300 apt-get $APT_OPTS install -y --no-install-recommends ffmpeg || echo "Skipped ffmpeg: the install did not finish"
78
+ `;
79
+ /**
80
+ * SHA-256 of the fixed installers unity-cli downloads, recorded from the files Unity and Ubuntu serve today.
81
+ * A download that does not match is deleted instead of installed.
82
+ */
83
+ const PINNED_DOWNLOAD_SHA256 = {
84
+ 'https://beta.unity3d.com/download/UnitySetup-4.7.2.exe': '30e462dfe9c2ef176e4d1c78742ab777099ae5cfab3b46502af6c426b7e4e8a7',
85
+ 'https://beta.unity3d.com/download/unity-4.7.2.dmg': '2fcaa916624b2eecc1686a943d5b7806fb5bdaf01df7b4a544103f114b85f40b',
86
+ 'https://archive.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.0.0_1.0.2g-1ubuntu4.20_amd64.deb': '8003f9866297dd0fe77cc771470a06803d1d1b860350ad91dd212e38c36a20d2',
87
+ 'https://archive.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.1_1.1.0g-2ubuntu4_amd64.deb': '82e424d50e144320319414a1964d3a980abde4bc39fc7d8c2885819328cb5cb4',
88
+ };
89
+ /** Unity 5.x target support installer names, keyed by module id. */
90
+ /** The PlaybackEngines folder each archive module installs into, where it differs from `<name>Support`. */
91
+ const ARCHIVE_PLAYBACK_ENGINE_FOLDERS = {
92
+ 'Android': 'AndroidPlayer',
93
+ 'Linux': 'LinuxStandaloneSupport',
94
+ 'Mac': 'MacStandaloneSupport',
95
+ 'Windows': 'WindowsStandaloneSupport',
96
+ };
97
+ const ARCHIVE_MODULE_NAMES = {
98
+ 'android': 'Android',
99
+ 'ios': 'iOS',
100
+ 'appletv': 'AppleTV',
101
+ 'webgl': 'WebGL',
102
+ 'linux': 'Linux',
103
+ 'linux-il2cpp': 'Linux',
104
+ 'mac': 'Mac',
105
+ 'mac-il2cpp': 'Mac',
106
+ 'windows': 'Windows',
107
+ 'windows-il2cpp': 'Windows',
108
+ 'universal-windows-platform': 'Metro',
109
+ 'facebook-games': 'Facebook-Games',
110
+ 'samsungtv': 'Samsung-TV',
111
+ 'tizen': 'Tizen',
112
+ };
113
+ /**
114
+ * Finds or installs Unity Editors.
115
+ * - 2017.4 and newer install through the official Unity CLI (`unity install`).
116
+ * - 5.x installs from Unity's download archive, because the official CLI reads one-digit majors as Unity 6 shorthand.
117
+ * - 4.x installs from Unity's legacy installers.
118
+ */
119
+ class UnityInstaller {
120
+ logger = logging_1.Logger.instance;
121
+ cli;
122
+ /** The 5.x release being installed from the archive, and its installers' published integrity values. */
123
+ archiveVersion;
124
+ archiveIntegrity;
125
+ /**
126
+ * @param cli The official Unity CLI to use. Installed on demand when omitted.
127
+ */
128
+ constructor(cli = undefined) {
129
+ this.cli = cli;
130
+ }
131
+ /**
132
+ * Gets the official Unity CLI, installing it when needed.
133
+ */
134
+ async GetCli() {
135
+ if (!this.cli) {
136
+ this.cli = await unity_cli_1.UnityCli.Get();
137
+ }
138
+ return this.cli;
139
+ }
140
+ /**
141
+ * Sets the directory editors are installed to.
142
+ * @param installPath The directory to install editors to.
143
+ */
144
+ async SetInstallPath(installPath) {
145
+ const cli = await this.GetCli();
146
+ await cli.SetInstallPath(path.resolve(installPath));
147
+ }
148
+ /**
149
+ * Gets the directory editors are installed to.
150
+ */
151
+ async GetInstallPath() {
152
+ const cli = await this.GetCli();
153
+ return await cli.GetInstallPath();
154
+ }
155
+ /**
156
+ * Finds the requested Unity Editor, installing it and any missing modules when needed.
157
+ * @param unityVersion The Unity version to find or install. Partial versions such as 2021.3.x resolve to the newest matching release.
158
+ * @param modules The module ids to install alongside the editor.
159
+ * @param channels The release channels to accept when resolving a partial version: f, p, b, a or x. Default is ['f'].
160
+ */
161
+ async GetEditor(unityVersion, modules = [], channels = ['f']) {
162
+ this.logger.ci(`Getting release info for Unity ${unityVersion.toString()}...`);
163
+ const resolvedVersion = await this.resolveVersion(unityVersion, channels);
164
+ let editorPath;
165
+ if (resolvedVersion.isLegacy()) {
166
+ editorPath = await this.installLegacyEditor(resolvedVersion);
167
+ }
168
+ else if (resolvedVersion.isLessThan('2017.0.0')) {
169
+ editorPath = await this.installArchiveEditor(resolvedVersion, modules);
170
+ }
171
+ else {
172
+ editorPath = await this.installCliEditor(resolvedVersion, modules);
173
+ }
174
+ await fs.promises.access(editorPath, fs.constants.X_OK);
175
+ await this.patchBeeBackend(editorPath);
176
+ return new unity_editor_1.UnityEditor(path.normalize(editorPath), resolvedVersion);
177
+ }
178
+ /**
179
+ * Resolves a partial or changeset-less version to an exact release with its changeset.
180
+ * 4.x versions are returned as they are, since no release service lists them.
181
+ * @param unityVersion The requested version.
182
+ * @param channels The release channels to accept.
183
+ */
184
+ async resolveVersion(unityVersion, channels = ['f']) {
185
+ if (unityVersion.isLegacy() || (unityVersion.isFullyQualified() && unityVersion.changeset)) {
186
+ return unityVersion;
187
+ }
188
+ try {
189
+ const release = await UnityInstaller.GetEditorReleaseInfo(unityVersion, channels);
190
+ return new unity_version_1.UnityVersion(release.version, release.shortRevision, unityVersion.architecture);
191
+ }
192
+ catch (error) {
193
+ // fail closed for partial versions: never install "6000.6" and hope it picks a release
194
+ if (!unityVersion.isFullyQualified()) {
195
+ const message = error instanceof Error ? error.message : String(error);
196
+ throw new Error(`Failed to resolve Unity ${unityVersion.toString()} for channel(s) [${channels.join(', ')}]: ${message}`);
197
+ }
198
+ this.logger.warn(`Failed to get Unity release info for ${unityVersion.toString()}! falling back to the release notes page...\n${error}`);
199
+ try {
200
+ return await UnityInstaller.fallbackVersionLookup(unityVersion);
201
+ }
202
+ catch (fallbackError) {
203
+ this.logger.warn(`Failed to lookup changeset for Unity ${unityVersion.toString()}!\n${fallbackError}`);
204
+ return unityVersion;
205
+ }
206
+ }
207
+ }
208
+ async installCliEditor(unityVersion, modules) {
209
+ const cli = await this.GetCli();
210
+ let editorPath = await this.findCliEditor(cli, unityVersion);
211
+ const requestedModules = modules.length > 0 ? await this.filterModules(cli, unityVersion, modules) : [];
212
+ if (!editorPath) {
213
+ if (process.platform === 'linux') {
214
+ await this.installLinuxDependencies(unityVersion);
215
+ }
216
+ this.logger.ci(`Installing Unity ${unityVersion.toString()}...`);
217
+ const args = ['install', unityVersion.version];
218
+ if (unityVersion.changeset) {
219
+ args.push('--changeset', unityVersion.changeset);
220
+ }
221
+ args.push('--architecture', unityVersion.architecture.toLowerCase());
222
+ if (requestedModules.length > 0) {
223
+ for (const module of requestedModules) {
224
+ this.logger.ci(` > with module: ${module}`);
225
+ }
226
+ args.push('--module', ...requestedModules, '--child-modules');
227
+ }
228
+ args.push('--accept-eula', '--yes');
229
+ await this.execWithRetry(cli, args);
230
+ editorPath = await this.findCliEditor(cli, unityVersion);
231
+ if (!editorPath) {
232
+ throw new Error(`Unity ${unityVersion.toString()} was installed, but its editor could not be found`);
233
+ }
234
+ }
235
+ else if (requestedModules.length > 0) {
236
+ this.logger.ci(`Installing modules for Unity ${unityVersion.toString()}: ${requestedModules.join(', ')}`);
237
+ await this.execWithRetry(cli, [
238
+ 'install-modules',
239
+ '--editor-version', unityVersion.version,
240
+ '--architecture', unityVersion.architecture.toLowerCase(),
241
+ '--module', ...requestedModules,
242
+ '--child-modules',
243
+ '--accept-eula',
244
+ '--yes'
245
+ ]);
246
+ }
247
+ if (unityVersion.isGreaterThanOrEqualTo('6000.0.0')) {
248
+ // so the editor opens without the terms dialog
249
+ const result = await cli.Run(['terms', 'accept', unityVersion.version, '--architecture', unityVersion.architecture.toLowerCase(), '--yes']);
250
+ if (result.exitCode !== 0) {
251
+ this.logger.debug(`unity terms accept exited with code ${result.exitCode}:\n${result.stdout}${result.stderr}`);
252
+ }
253
+ }
254
+ return editorPath;
255
+ }
256
+ async execWithRetry(cli, args) {
257
+ let result = await cli.Run(args, { stream: true });
258
+ if (RETRYABLE_EXIT_CODES.includes(result.exitCode)) {
259
+ this.logger.warn(`unity ${args[0]} failed with exit code ${result.exitCode}, retrying...`);
260
+ result = await cli.Run(args[0] === 'install' ? [...args, '--resume'] : args, { stream: true });
261
+ }
262
+ // install-modules fails when every requested module is already installed
263
+ if (result.exitCode !== 0 && args[0] === 'install-modules' && `${result.stdout}${result.stderr}`.includes('No modules found to install')) {
264
+ this.logger.info('The requested modules are already installed.');
265
+ return;
266
+ }
267
+ if (result.exitCode !== 0) {
268
+ throw new Error(`unity ${args.join(' ')} failed with exit code ${result.exitCode}`);
269
+ }
270
+ }
271
+ /**
272
+ * Drops modules the release does not offer, since `unity install` fails on an unknown module id.
273
+ */
274
+ async filterModules(cli, unityVersion, modules) {
275
+ const { exitCode, envelope } = await cli.RunJson(['editors', 'info', unityVersion.version]);
276
+ const available = envelope?.data?.modules?.map(module => module.id);
277
+ if (exitCode !== 0 || !available || available.length === 0) {
278
+ this.logger.debug(`Could not read the module list for Unity ${unityVersion.version}, requesting modules as given`);
279
+ return [...modules];
280
+ }
281
+ const supported = [];
282
+ for (const module of modules) {
283
+ if (available.includes(module)) {
284
+ supported.push(module);
285
+ }
286
+ else {
287
+ this.logger.warn(`Unity ${unityVersion.version} has no ${module} module on ${process.platform}, skipping it`);
288
+ }
289
+ }
290
+ return supported;
291
+ }
292
+ /**
293
+ * Looks up an installed editor with `unity editors path`, registering one that sits in the install path but that the CLI does not know about yet.
294
+ */
295
+ async findCliEditor(cli, unityVersion) {
296
+ const { exitCode, envelope } = await cli.RunJson(['editors', 'path', unityVersion.version]);
297
+ // editors path takes no architecture, so the binary is checked to not return another architecture's install
298
+ const matchesArchitecture = (editorPath) => {
299
+ const architectures = getExecutableArchitectures(editorPath);
300
+ const matches = !architectures || architectures.includes(unityVersion.architecture);
301
+ if (!matches) {
302
+ this.logger.debug(`Skipping ${editorPath}: built for ${architectures.join(', ')}, not ${unityVersion.architecture}`);
303
+ }
304
+ return matches;
305
+ };
306
+ if (exitCode === 0 && envelope?.success !== false) {
307
+ const editorPath = resolveEditorExecutable((0, unity_cli_1.FindAbsolutePath)(envelope?.data));
308
+ if (editorPath && matchesArchitecture(editorPath)) {
309
+ this.logger.debug(`Found installed editor: "${editorPath}"`);
310
+ return editorPath;
311
+ }
312
+ }
313
+ let installPath;
314
+ try {
315
+ installPath = await cli.GetInstallPath();
316
+ }
317
+ catch (error) {
318
+ this.logger.debug(`${error}`);
319
+ return undefined;
320
+ }
321
+ // the architecture-specific folder first; the plain one only when its binary is the requested architecture
322
+ const candidates = [
323
+ path.join(installPath, `${unityVersion.version}-${unityVersion.architecture.toLowerCase()}`),
324
+ path.join(installPath, unityVersion.version),
325
+ ];
326
+ for (const candidate of candidates) {
327
+ const editorPath = resolveEditorExecutable(candidate);
328
+ if (editorPath && matchesArchitecture(editorPath)) {
329
+ this.logger.info(`Registering existing Unity ${unityVersion.version} at ${editorPath}`);
330
+ const result = await cli.Run(['editors', 'add', editorPath]);
331
+ if (result.exitCode !== 0) {
332
+ this.logger.warn(`unity editors add exited with code ${result.exitCode}`);
333
+ }
334
+ return editorPath;
335
+ }
336
+ }
337
+ return undefined;
338
+ }
339
+ async getLegacyInstallPath() {
340
+ try {
341
+ return await this.GetInstallPath();
342
+ }
343
+ catch (error) {
344
+ this.logger.debug(`Falling back to the default editor install path: ${error}`);
345
+ switch (process.platform) {
346
+ case 'win32':
347
+ return path.join(process.env.ProgramFiles || 'C:\\Program Files', 'Unity', 'Hub', 'Editor');
348
+ case 'darwin':
349
+ return '/Applications/Unity/Hub/Editor';
350
+ default:
351
+ return path.join(os.homedir(), 'Unity', 'Hub', 'Editor');
352
+ }
353
+ }
354
+ }
355
+ /**
356
+ * Installs Unity 5.x from the download archive.
357
+ */
358
+ async installArchiveEditor(unityVersion, modules) {
359
+ if (!unityVersion.changeset) {
360
+ throw new Error(`Unity ${unityVersion.toString()} needs a changeset to install from the download archive`);
361
+ }
362
+ UnityInstaller.assertArchiveVersion(unityVersion);
363
+ this.archiveVersion = unityVersion;
364
+ this.archiveIntegrity = undefined;
365
+ const installPath = await this.getLegacyInstallPath();
366
+ const installDir = path.join(installPath, unityVersion.version);
367
+ const baseUrl = `https://download.unity3d.com/download_unity/${unityVersion.changeset}`;
368
+ const moduleNames = this.getArchiveModuleNames(unityVersion, modules);
369
+ switch (process.platform) {
370
+ case 'win32': {
371
+ let editorPath = resolveEditorExecutable(installDir);
372
+ if (!editorPath) {
373
+ this.logger.ci(`Installing Unity ${unityVersion.toString()}...`);
374
+ await this.runWindowsInstaller(`${baseUrl}/Windows64EditorInstaller/UnitySetup64-${unityVersion.version}.exe`, installDir);
375
+ editorPath = resolveEditorExecutable(installDir);
376
+ }
377
+ if (!editorPath) {
378
+ throw new Error(`Failed to install Unity ${unityVersion.toString()} to ${installDir}`);
379
+ }
380
+ for (const moduleName of moduleNames) {
381
+ const engineFolder = ARCHIVE_PLAYBACK_ENGINE_FOLDERS[moduleName] ?? `${moduleName}Support`;
382
+ // Windows paths are case-insensitive, so the folder name's case does not matter here
383
+ if (fs.existsSync(path.join(installDir, 'Editor', 'Data', 'PlaybackEngines', engineFolder))) {
384
+ continue;
385
+ }
386
+ this.logger.ci(` > with module: ${moduleName}`);
387
+ await this.runWindowsInstaller(`${baseUrl}/TargetSupportInstaller/UnitySetup-${moduleName}-Support-for-Editor-${unityVersion.version}.exe`, installDir);
388
+ }
389
+ return editorPath;
390
+ }
391
+ case 'darwin': {
392
+ let editorPath = resolveEditorExecutable(installDir);
393
+ const modulePackages = moduleNames.map(name => `${baseUrl}/MacEditorTargetInstaller/UnitySetup-${name}-Support-for-Editor-${unityVersion.version}.pkg`);
394
+ if (editorPath) {
395
+ if (modulePackages.length > 0) {
396
+ // the target support packages install into /Applications/Unity/PlaybackEngines, so they are moved into this editor afterwards
397
+ this.logger.ci(`Installing modules for Unity ${unityVersion.toString()}: ${moduleNames.join(', ')}`);
398
+ await this.installMacPackages(modulePackages);
399
+ await this.moveMacPlaybackEngines(installDir);
400
+ }
401
+ return editorPath;
402
+ }
403
+ this.logger.ci(`Installing Unity ${unityVersion.toString()}...`);
404
+ await this.installMacPackages([`${baseUrl}/MacEditorInstaller/Unity-${unityVersion.version}.pkg`, ...modulePackages]);
405
+ await this.moveMacApplicationsUnity(installDir);
406
+ editorPath = resolveEditorExecutable(installDir);
407
+ if (!editorPath) {
408
+ throw new Error(`Failed to install Unity ${unityVersion.toString()} to ${installDir}`);
409
+ }
410
+ return editorPath;
411
+ }
412
+ default:
413
+ throw new Error(`Unity ${unityVersion.toString()} is not supported on ${process.platform}`);
414
+ }
415
+ }
416
+ /**
417
+ * Gets the checksum a download must match before it is installed: a pinned SHA-256 for the fixed installers,
418
+ * or the Unity Releases API integrity value for 5.x archive installers. Fails closed when neither is known.
419
+ * @param url The download URL.
420
+ */
421
+ async getDownloadOptions(url) {
422
+ const pinned = PINNED_DOWNLOAD_SHA256[url];
423
+ if (pinned) {
424
+ return { sha256: pinned };
425
+ }
426
+ const archive = url.match(/^https:\/\/download\.unity3d\.com\/download_unity\/([0-9a-f]{12})\//);
427
+ if (archive && this.archiveVersion) {
428
+ if (!this.archiveIntegrity) {
429
+ this.archiveIntegrity = await UnityInstaller.getReleaseIntegrity(this.archiveVersion);
430
+ }
431
+ const integrity = this.archiveIntegrity.get(url);
432
+ if (integrity) {
433
+ return { integrity };
434
+ }
435
+ }
436
+ throw new Error(`No checksum is known for ${url}, so it is not installed`);
437
+ }
438
+ /**
439
+ * Maps each installer URL of a release (editor and modules) to the integrity value the Unity Releases API publishes for it.
440
+ * @param unityVersion The exact release.
441
+ */
442
+ static async getReleaseIntegrity(unityVersion) {
443
+ const integrity = new Map();
444
+ const releasesClient = new unity_releases_api_1.UnityReleasesClient();
445
+ const { data, error } = await releasesClient.api.Release.getUnityReleases({
446
+ url: '/unity/editor/release/v1/releases',
447
+ query: { version: unityVersion.version, limit: 5 },
448
+ });
449
+ if (error) {
450
+ throw new Error(`Failed to read the checksums for Unity ${unityVersion.version}: ${JSON.stringify(error)}`);
451
+ }
452
+ const collect = (entries) => {
453
+ if (!Array.isArray(entries)) {
454
+ return;
455
+ }
456
+ for (const entry of entries) {
457
+ if (entry.url && entry.integrity) {
458
+ integrity.set(entry.url, entry.integrity);
459
+ }
460
+ collect(entry.modules);
461
+ }
462
+ };
463
+ for (const release of data?.results ?? []) {
464
+ if (release.version === unityVersion.version) {
465
+ collect(release.downloads);
466
+ }
467
+ }
468
+ return integrity;
469
+ }
470
+ getArchiveModuleNames(unityVersion, modules) {
471
+ const hostModule = process.platform === 'win32' ? 'Windows' : process.platform === 'darwin' ? 'Mac' : 'Linux';
472
+ const names = [];
473
+ for (const module of modules) {
474
+ const name = ARCHIVE_MODULE_NAMES[module];
475
+ if (!name) {
476
+ this.logger.warn(`Unity ${unityVersion.version} has no ${module} module in the download archive, skipping it`);
477
+ }
478
+ else if (name !== hostModule && !names.includes(name)) {
479
+ names.push(name);
480
+ }
481
+ }
482
+ return names;
483
+ }
484
+ /**
485
+ * Runs a Unity NSIS installer silently. NSIS needs `/D=` last and unquoted, so the command line is passed verbatim, without a shell.
486
+ */
487
+ async runWindowsInstaller(url, installDir) {
488
+ if (/["\r\n]/.test(installDir)) {
489
+ throw new Error(`Invalid install directory: ${installDir}`);
490
+ }
491
+ const installerPath = tempInstallerPath('.exe');
492
+ await (0, utilities_1.DownloadFile)(url, installerPath, await this.getDownloadOptions(url));
493
+ this.logger.info(`Running ${path.basename(url)}...`);
494
+ try {
495
+ const exitCode = await new Promise((resolve, reject) => {
496
+ const child = (0, child_process_1.spawn)(installerPath, ['/S', `/D=${installDir}`], {
497
+ shell: false,
498
+ stdio: 'ignore',
499
+ windowsVerbatimArguments: true,
500
+ });
501
+ child.on('error', reject);
502
+ child.on('close', (code) => resolve(code ?? 1));
503
+ });
504
+ if (exitCode !== 0) {
505
+ throw new Error(`${path.basename(url)} failed with exit code ${exitCode}`);
506
+ }
507
+ }
508
+ finally {
509
+ await fs.promises.rm(installerPath, { force: true });
510
+ }
511
+ }
512
+ async installMacPackages(urls) {
513
+ for (const url of urls) {
514
+ const pkgPath = tempInstallerPath('.pkg');
515
+ await (0, utilities_1.DownloadFile)(url, pkgPath, await this.getDownloadOptions(url));
516
+ try {
517
+ await (0, utilities_1.Exec)('sudo', ['installer', '-pkg', pkgPath, '-target', '/'], { silent: true, showCommand: true });
518
+ }
519
+ finally {
520
+ await fs.promises.rm(pkgPath, { force: true });
521
+ }
522
+ }
523
+ }
524
+ /**
525
+ * Moves target support that macOS packages installed to /Applications/Unity/PlaybackEngines into an existing versioned editor.
526
+ */
527
+ async moveMacPlaybackEngines(targetPath) {
528
+ const sourcePath = path.join('/Applications', 'Unity', 'PlaybackEngines');
529
+ if (!fs.existsSync(sourcePath)) {
530
+ throw new Error(`The module packages did not install anything into ${sourcePath}`);
531
+ }
532
+ const destinationPath = path.join(targetPath, 'PlaybackEngines');
533
+ await fs.promises.mkdir(destinationPath, { recursive: true });
534
+ // the parent too, since removing PlaybackEngines afterwards needs write permission on it
535
+ await claimMacInstalledItem(path.dirname(sourcePath), false);
536
+ await claimMacInstalledItem(sourcePath, true);
537
+ for (const item of await fs.promises.readdir(sourcePath)) {
538
+ await moveMacInstalledItem(path.join(sourcePath, item), path.join(destinationPath, item));
539
+ }
540
+ await fs.promises.rm(sourcePath, { recursive: true, force: true });
541
+ }
542
+ /**
543
+ * Moves an editor that a macOS package installed to /Applications/Unity into its versioned folder.
544
+ */
545
+ async moveMacApplicationsUnity(targetPath) {
546
+ const unityAppPath = path.join('/Applications', 'Unity');
547
+ if (!fs.existsSync(path.join(unityAppPath, 'Unity.app'))) {
548
+ throw new Error('Failed to find /Applications/Unity/Unity.app after installation.');
549
+ }
550
+ this.logger.debug(`Moving ${unityAppPath} to ${targetPath}...`);
551
+ await fs.promises.mkdir(targetPath, { recursive: true });
552
+ // the folder itself, so its entries can be moved out; the Hub folder inside it is left as it is
553
+ await claimMacInstalledItem(unityAppPath, false);
554
+ for (const item of await fs.promises.readdir(unityAppPath)) {
555
+ // the editor install path itself lives in /Applications/Unity/Hub
556
+ if (item === 'Hub') {
557
+ continue;
558
+ }
559
+ const source = path.join(unityAppPath, item);
560
+ await claimMacInstalledItem(source, true);
561
+ await moveMacInstalledItem(source, path.join(targetPath, item));
562
+ }
563
+ await fs.promises.chmod(targetPath, 0o755);
564
+ }
565
+ /**
566
+ * Rejects versions and changesets that are not plain Unity release identifiers before they become URLs and paths.
567
+ */
568
+ static assertArchiveVersion(unityVersion) {
569
+ if (!/^\d{1,2}\.\d{1,2}\.\d{1,2}(?:[abcfpx]\d{1,3})?$/.test(unityVersion.version)) {
570
+ throw new Error(`Invalid Unity version: ${unityVersion.version}`);
571
+ }
572
+ if (unityVersion.changeset && !/^[0-9a-f]{12}$/i.test(unityVersion.changeset)) {
573
+ throw new Error(`Invalid Unity changeset: ${unityVersion.changeset}`);
574
+ }
575
+ }
576
+ /**
577
+ * Installs Unity 4.x from Unity's legacy installers.
578
+ */
579
+ async installLegacyEditor(unityVersion) {
580
+ UnityInstaller.assertArchiveVersion(unityVersion);
581
+ const installPath = await this.getLegacyInstallPath();
582
+ const installDir = path.join(installPath, `Unity ${unityVersion.version}`);
583
+ let editorPath = resolveEditorExecutable(installDir);
584
+ if (editorPath) {
585
+ return editorPath;
586
+ }
587
+ this.logger.ci(`Installing Unity ${unityVersion.toString()}...`);
588
+ switch (process.platform) {
589
+ case 'win32': {
590
+ await this.runWindowsInstaller(`https://beta.unity3d.com/download/UnitySetup-${unityVersion.version}.exe`, installDir);
591
+ break;
592
+ }
593
+ case 'darwin': {
594
+ const url = `https://beta.unity3d.com/download/unity-${unityVersion.version}.dmg`;
595
+ const installerPath = tempInstallerPath('.dmg');
596
+ await (0, utilities_1.DownloadFile)(url, installerPath, await this.getDownloadOptions(url));
597
+ let mountPoint = '';
598
+ try {
599
+ const output = await (0, utilities_1.Exec)('hdiutil', ['attach', installerPath, '-nobrowse'], { silent: true, showCommand: true });
600
+ const mountPointMatch = output.match(/\/Volumes\/Unity Installer.*$/m);
601
+ if (!mountPointMatch || mountPointMatch.length === 0) {
602
+ throw new Error(`Failed to find mount point in hdiutil output: ${output}`);
603
+ }
604
+ mountPoint = mountPointMatch[0];
605
+ const pkgPath = path.join(mountPoint, 'Unity.pkg');
606
+ await fs.promises.access(pkgPath, fs.constants.R_OK);
607
+ await (0, utilities_1.Exec)('sudo', ['installer', '-pkg', pkgPath, '-target', '/', '-verboseR'], { silent: true, showCommand: true });
608
+ await this.moveMacApplicationsUnity(installDir);
609
+ }
610
+ finally {
611
+ try {
612
+ if (mountPoint.length > 0) {
613
+ await (0, utilities_1.Exec)('hdiutil', ['detach', mountPoint, '-quiet'], { silent: true, showCommand: true });
614
+ }
615
+ }
616
+ finally {
617
+ await fs.promises.rm(installerPath, { force: true });
618
+ }
619
+ }
620
+ break;
621
+ }
622
+ default:
623
+ throw new Error(`Unity ${unityVersion.toString()} is not supported on ${process.platform}`);
624
+ }
625
+ editorPath = resolveEditorExecutable(installDir);
626
+ if (!editorPath) {
627
+ throw new Error(`Failed to install Unity ${unityVersion.toString()} to ${installDir}`);
628
+ }
629
+ return editorPath;
630
+ }
631
+ /**
632
+ * Installs the system packages the Linux editor needs, and the older libssl that 2019.1 to 2021.x link against.
633
+ */
634
+ async installLinuxDependencies(unityVersion) {
635
+ if (process.env.UNITY_CLI_LINUX_DEPENDENCIES_INSTALLED !== 'true') {
636
+ try {
637
+ // streamed, so a slow mirror shows where it stalls
638
+ // sudo drops the caller's environment, so the opt-in is read here rather than in the script
639
+ const ffmpeg = process.env.UNITY_CLI_LINUX_FFMPEG === 'true' ? LINUX_FFMPEG_SCRIPT : '';
640
+ await (0, utilities_1.Exec)('sudo', ['sh', '-c', `${LINUX_EDITOR_DEPENDENCIES_SCRIPT}${ffmpeg}apt-get clean\n`], { silent: false, showCommand: true });
641
+ process.env.UNITY_CLI_LINUX_DEPENDENCIES_INSTALLED = 'true';
642
+ this.logger.CI_setEnvironmentVariable('UNITY_CLI_LINUX_DEPENDENCIES_INSTALLED', 'true');
643
+ }
644
+ catch (error) {
645
+ this.logger.warn(`Failed to install Linux editor dependencies: ${error}`);
646
+ }
647
+ }
648
+ const arch = process.arch === 'x64' ? 'amd64' : process.arch;
649
+ let debName;
650
+ if (['2019.1', '2019.2'].some(v => unityVersion.version.startsWith(v))) {
651
+ debName = `libssl1.0.0_1.0.2g-1ubuntu4.20_${arch}.deb`;
652
+ }
653
+ else if (['2019.3', '2019.4'].some(v => unityVersion.version.startsWith(v)) ||
654
+ unityVersion.version.startsWith('2020.') ||
655
+ unityVersion.version.startsWith('2021.')) {
656
+ debName = `libssl1.1_1.1.0g-2ubuntu4_${arch}.deb`;
657
+ }
658
+ if (debName) {
659
+ const downloadPath = tempInstallerPath('.deb');
660
+ const url = `https://archive.ubuntu.com/ubuntu/pool/main/o/openssl/${debName}`;
661
+ await (0, utilities_1.DownloadFile)(url, downloadPath, await this.getDownloadOptions(url));
662
+ try {
663
+ await (0, utilities_1.Exec)('sudo', ['dpkg', '-i', downloadPath]);
664
+ }
665
+ finally {
666
+ await fs.promises.rm(downloadPath, { force: true });
667
+ }
668
+ }
669
+ }
670
+ /**
671
+ * Patches the Bee Backend for Unity Linux Editor.
672
+ * https://discussions.unity.com/t/linux-editor-stuck-on-loading-because-of-bee-backend-w-workaround/854480
673
+ * @param editorPath The path to the Unity Editor executable.
674
+ */
675
+ async patchBeeBackend(editorPath) {
676
+ if (process.platform === 'linux') {
677
+ const dataPath = path.join(path.dirname(editorPath), 'Data');
678
+ const beeBackend = path.join(dataPath, 'bee_backend');
679
+ const dotBeeBackend = path.join(dataPath, '.bee_backend');
680
+ // linking fails with EEXIST once patched and ENOENT without a bee_backend, so nothing is checked before it is used
681
+ try {
682
+ await fs.promises.link(beeBackend, dotBeeBackend);
683
+ }
684
+ catch (error) {
685
+ const code = error.code;
686
+ if (code === 'ENOENT') {
687
+ return;
688
+ }
689
+ if (code !== 'EEXIST') {
690
+ throw error;
691
+ }
692
+ // patched before; only a run that stopped before the wrapper landed leaves .bee_backend without bee_backend
693
+ try {
694
+ await fs.promises.access(beeBackend);
695
+ return;
696
+ }
697
+ catch {
698
+ this.logger.ci('Restoring the Bee Backend wrapper an earlier run did not finish...');
699
+ }
700
+ }
701
+ this.logger.ci(`Patching Unity Linux Editor for Bee Backend...`);
702
+ const wrapperSource = `#!/bin/bash
703
+ # https://discussions.unity.com/t/linux-editor-stuck-on-loading-because-of-bee-backend-w-workaround/854480
704
+ set -e
705
+ args=("$@")
706
+ for ((i = 0; i < "\${#args[@]}"; ++i)); do
707
+ case \${args[i]} in
708
+ --stdin-canary)
709
+ unset "args[i]"
710
+ break
711
+ ;;
712
+ esac
713
+ done
714
+ "$(dirname "$0")/.$(basename "$0")" "\${args[@]}"`;
715
+ // written under a temporary name and renamed over bee_backend, so a stop at any point leaves either the
716
+ // original (still linked as .bee_backend) or the whole wrapper, never neither
717
+ const tempWrapper = path.join(dataPath, `.bee_backend.${(0, crypto_1.randomUUID)()}.tmp`);
718
+ await fs.promises.writeFile(tempWrapper, wrapperSource, { encoding: 'utf-8', mode: 0o755, flag: 'wx' });
719
+ await fs.promises.rename(tempWrapper, beeBackend);
720
+ }
721
+ }
722
+ /**
723
+ * Gets the specified Unity release info from the Unity Releases API.
724
+ * Supports querying by exact version or by prefix (e.g., "2020", "2020.1", "2021.x", "2021.3.x").
725
+ * @param unityVersion The Unity version to get the release info for.
726
+ * @param channels Letter channels to accept (`f`, `p`, `b`, `a`, `x`). Default stable-only.
727
+ * @returns The Unity release info.
728
+ */
729
+ static async GetEditorReleaseInfo(unityVersion, channels = ['f']) {
730
+ // Prefer querying the releases API with the exact fully-qualified Unity version (e.g., 2022.3.10f1).
731
+ // If we don't have a fully-qualified version, use the most specific prefix available:
732
+ // - "YYYY.M" when provided (e.g., 6000.1)
733
+ // - otherwise "YYYY"
734
+ const fullUnityVersionPattern = /^\d{1,4}\.\d+\.\d+[abcfpx]\d+$/;
735
+ let version;
736
+ if (fullUnityVersionPattern.test(unityVersion.version)) {
737
+ version = unityVersion.version;
738
+ }
739
+ else {
740
+ const match = unityVersion.version.match(/^(\d{1,4})(?:\.(\d+))?/);
741
+ if (match) {
742
+ version = match[2] ? `${match[1]}.${match[2]}` : match[1];
743
+ }
744
+ else {
745
+ version = unityVersion.version.split('.')[0];
746
+ }
747
+ }
748
+ const releasesClient = new unity_releases_api_1.UnityReleasesClient();
749
+ const channelSet = new Set(channels.map(c => c.toLowerCase()));
750
+ function getPlatform() {
751
+ switch (process.platform) {
752
+ case 'darwin':
753
+ return ['MAC_OS'];
754
+ case 'linux':
755
+ return ['LINUX'];
756
+ case 'win32':
757
+ return ['WINDOWS'];
758
+ default:
759
+ throw new Error(`Unsupported platform: ${process.platform}`);
760
+ }
761
+ }
762
+ function releaseChannelLetter(releaseVersion) {
763
+ const m = /^(\d{1,4})\.(\d+)\.(\d+)([abcfpx])(\d+)$/.exec(releaseVersion);
764
+ return m?.[4];
765
+ }
766
+ const request = {
767
+ url: '/unity/editor/release/v1/releases',
768
+ query: {
769
+ version: version,
770
+ architecture: [unityVersion.architecture],
771
+ platform: getPlatform(),
772
+ limit: 10,
773
+ order: 'RELEASE_DATE_DESC',
774
+ }
775
+ };
776
+ logging_1.Logger.instance.debug(`Get Unity Release: ${JSON.stringify(request, null, 2)}`);
777
+ async function getRelease() {
778
+ const { data, error } = await releasesClient.api.Release.getUnityReleases(request);
779
+ if (error) {
780
+ throw new Error(`Failed to get Unity releases: ${JSON.stringify(error, null, 2)}`);
781
+ }
782
+ if (!data || !data.results || data.results.length === 0) {
783
+ throw new Error(`No Unity releases found for version: ${version}`);
784
+ }
785
+ const releases = (data.results || [])
786
+ .filter((release) => {
787
+ const v = release.version;
788
+ if (v == null || v === '') {
789
+ return false;
790
+ }
791
+ // Exact FQ request: accept that row regardless of channel filter.
792
+ if (fullUnityVersionPattern.test(unityVersion.version) && v === unityVersion.version) {
793
+ return true;
794
+ }
795
+ const letter = releaseChannelLetter(v);
796
+ return letter != null && channelSet.has(letter);
797
+ })
798
+ .map(release => ({
799
+ unityRelease: release,
800
+ unityVersion: new unity_version_1.UnityVersion(release.version, release.shortRevision, unityVersion.architecture)
801
+ }));
802
+ if (releases.length === 0) {
803
+ const channelList = [...channelSet].join(',');
804
+ throw new Error(`No suitable Unity releases (channels: ${channelList}) found for version: ${version}` +
805
+ (channelSet.has('f') && channelSet.size === 1
806
+ ? `. No stable (f) release for ${version}; use --channel b/a or a fully-qualified version.`
807
+ : ''));
808
+ }
809
+ releases.sort((a, b) => unity_version_1.UnityVersion.compare(b.unityVersion, a.unityVersion));
810
+ logging_1.Logger.instance.debug(`Found ${releases.length} matching Unity releases for version: ${version}`);
811
+ releases.forEach(release => {
812
+ logging_1.Logger.instance.debug(` - ${release.unityRelease.version} (${release.unityRelease.shortRevision}) - ${release.unityRelease.recommended}`);
813
+ });
814
+ return releases[0].unityRelease;
815
+ }
816
+ try {
817
+ return await getRelease();
818
+ }
819
+ catch (error) {
820
+ if (error instanceof Error && error.message.includes('fetch failed')) {
821
+ // Transient network error, retry once
822
+ return await getRelease();
823
+ }
824
+ throw new Error(`Failed to get Unity releases: ${error}`);
825
+ }
826
+ }
827
+ static async fallbackVersionLookup(unityVersion) {
828
+ const url = `https://unity.com/releases/editor/whats-new/${unityVersion.version}`;
829
+ logging_1.Logger.instance.debug(`Fetching release page: "${url}"`);
830
+ const response = await fetch(url);
831
+ const responseText = await response.text();
832
+ if (!response.ok) {
833
+ throw new Error(`Failed to fetch changeset for Unity ${unityVersion.toString()} [${response.status}] "${url}"`);
834
+ }
835
+ const match = responseText.match(/unityhub:\/\/(?<version>\d+\.\d+\.\d+[abcfpx]?\d*)\/(?<changeset>[a-zA-Z0-9]+)/);
836
+ if (match && match.groups && match.groups.version && match.groups.changeset) {
837
+ return new unity_version_1.UnityVersion(match.groups.version, match.groups.changeset, unityVersion.architecture);
838
+ }
839
+ throw new Error(`Failed to find changeset for Unity ${unityVersion.toString()} in "${url}"`);
840
+ }
841
+ /**
842
+ * Get the mapping of Unity platform targets to their corresponding module identifiers for the current OS.
843
+ * @returns A map of Unity platform targets to their corresponding module identifiers for the current OS.
844
+ */
845
+ static GetPlatformTargetModuleMap() {
846
+ const osType = os.type();
847
+ switch (osType) {
848
+ case 'Linux':
849
+ return {
850
+ "StandaloneLinux64": "linux-il2cpp",
851
+ "Android": "android",
852
+ "WebGL": "webgl",
853
+ "iOS": "ios",
854
+ };
855
+ case 'Darwin':
856
+ return {
857
+ "StandaloneOSX": "mac-il2cpp",
858
+ "iOS": "ios",
859
+ "Android": "android",
860
+ "tvOS": "appletv",
861
+ "StandaloneLinux64": "linux-il2cpp",
862
+ "WebGL": "webgl",
863
+ "VisionOS": "visionos"
864
+ };
865
+ case 'Windows_NT':
866
+ return {
867
+ "StandaloneWindows64": "windows-il2cpp",
868
+ "WSAPlayer": "universal-windows-platform",
869
+ "Android": "android",
870
+ "iOS": "ios",
871
+ "tvOS": "appletv",
872
+ "StandaloneLinux64": "linux-il2cpp",
873
+ "Lumin": "lumin",
874
+ "WebGL": "webgl",
875
+ };
876
+ default:
877
+ throw Error(`${osType} not supported`);
878
+ }
879
+ }
880
+ }
881
+ exports.UnityInstaller = UnityInstaller;
882
+ /**
883
+ * Resolves an install directory, Editor folder, app bundle or executable to the editor executable.
884
+ * @param location A path the editor may be under.
885
+ * @returns The editor executable, or undefined when none is found.
886
+ */
887
+ function resolveEditorExecutable(location) {
888
+ if (!location || !fs.existsSync(location)) {
889
+ return undefined;
890
+ }
891
+ if (fs.statSync(location).isFile()) {
892
+ return location;
893
+ }
894
+ let candidates;
895
+ switch (process.platform) {
896
+ case 'darwin':
897
+ candidates = [
898
+ path.join(location, 'Unity.app', 'Contents', 'MacOS', 'Unity'),
899
+ path.join(location, 'Contents', 'MacOS', 'Unity'),
900
+ ];
901
+ break;
902
+ case 'win32':
903
+ candidates = [
904
+ path.join(location, 'Editor', 'Unity.exe'),
905
+ path.join(location, 'Unity.exe'),
906
+ ];
907
+ break;
908
+ default:
909
+ candidates = [
910
+ path.join(location, 'Editor', 'Unity'),
911
+ path.join(location, 'Unity'),
912
+ ];
913
+ break;
914
+ }
915
+ return candidates.find(candidate => fs.existsSync(candidate) && fs.statSync(candidate).isFile());
916
+ }
917
+ /**
918
+ * A fresh temp path for a downloaded installer. The name is random rather than taken from the URL,
919
+ * so nothing from the requested version reaches the installer command line.
920
+ * @param extension The installer's file extension, e.g. .pkg.
921
+ */
922
+ function tempInstallerPath(extension) {
923
+ return path.join((0, utilities_1.GetTempDir)(), `unity-installer-${(0, crypto_1.randomUUID)()}${extension}`);
924
+ }
925
+ /**
926
+ * Gives the runner's user something a macOS package installed under /Applications/Unity. The installer runs as root,
927
+ * so its files are root-owned and cannot be moved or removed otherwise, such as PlaybackEngines/AndroidPlayer.
928
+ * @param source A path under /Applications/Unity, never one from a caller, since it runs as root.
929
+ * @param recursive Whether to claim everything below it too.
930
+ */
931
+ async function claimMacInstalledItem(source, recursive) {
932
+ if (!(0, process_tracker_1.IsPathUnder)(source, path.join('/Applications', 'Unity'))) {
933
+ throw new Error(`Refusing to take ownership of ${source}`);
934
+ }
935
+ const { uid, gid } = os.userInfo();
936
+ // -h changes a symlink itself, so a broken one does not fail the walk
937
+ await (0, utilities_1.Exec)('sudo', ['chown', ...(recursive ? ['-R'] : []), '-h', `${uid}:${gid}`, source], { silent: true, showCommand: true });
938
+ await (0, utilities_1.Exec)('sudo', ['chmod', ...(recursive ? ['-R'] : []), 'u+w', source], { silent: true, showCommand: true });
939
+ }
940
+ /**
941
+ * Moves something a macOS package installed. Older editors ship symlinks whose targets are gone, such as 5.6's
942
+ * MonoDevelop.app, which fs.cp fails on, so links are moved or recreated as they are.
943
+ */
944
+ async function moveMacInstalledItem(source, destination) {
945
+ await fs.promises.rm(destination, { recursive: true, force: true });
946
+ try {
947
+ await fs.promises.rename(source, destination);
948
+ }
949
+ catch (error) {
950
+ // the install path can be on another volume than /Applications
951
+ if (error.code !== 'EXDEV') {
952
+ throw error;
953
+ }
954
+ await copyKeepingSymlinks(source, destination);
955
+ await fs.promises.rm(source, { recursive: true, force: true });
956
+ }
957
+ }
958
+ async function copyKeepingSymlinks(source, destination) {
959
+ const stat = await fs.promises.lstat(source);
960
+ if (stat.isSymbolicLink()) {
961
+ await fs.promises.symlink(await fs.promises.readlink(source), destination);
962
+ }
963
+ else if (stat.isDirectory()) {
964
+ await fs.promises.mkdir(destination, { mode: stat.mode & 0o7777 });
965
+ for (const entry of await fs.promises.readdir(source)) {
966
+ await copyKeepingSymlinks(path.join(source, entry), path.join(destination, entry));
967
+ }
968
+ }
969
+ else {
970
+ await fs.promises.copyFile(source, destination);
971
+ await fs.promises.chmod(destination, stat.mode & 0o7777);
972
+ }
973
+ }
974
+ /**
975
+ * Reads the CPU architectures of a macOS editor binary from its Mach-O header.
976
+ * @param executablePath The editor executable.
977
+ * @returns The architectures, or undefined off macOS or when the header is not recognized, in which case callers accept the editor.
978
+ */
979
+ function getExecutableArchitectures(executablePath) {
980
+ if (process.platform !== 'darwin') {
981
+ return undefined;
982
+ }
983
+ const cpuTypes = { 0x01000007: 'X86_64', 0x0100000c: 'ARM64' };
984
+ let header;
985
+ try {
986
+ const fd = fs.openSync(executablePath, 'r');
987
+ try {
988
+ header = Buffer.alloc(4096);
989
+ fs.readSync(fd, header, 0, header.length, 0);
990
+ }
991
+ finally {
992
+ fs.closeSync(fd);
993
+ }
994
+ }
995
+ catch {
996
+ return undefined;
997
+ }
998
+ const magic = header.readUInt32BE(0);
999
+ if (magic === 0xcafebabe) {
1000
+ // fat binary: big-endian count, then 20 bytes per architecture with the CPU type first
1001
+ const count = header.readUInt32BE(4);
1002
+ const architectures = [];
1003
+ for (let i = 0; i < count && 8 + i * 20 + 4 <= header.length; i++) {
1004
+ const architecture = cpuTypes[header.readUInt32BE(8 + i * 20)];
1005
+ if (architecture && !architectures.includes(architecture)) {
1006
+ architectures.push(architecture);
1007
+ }
1008
+ }
1009
+ return architectures.length > 0 ? architectures : undefined;
1010
+ }
1011
+ if (magic === 0xcffaedfe) {
1012
+ // thin 64-bit binary, little-endian
1013
+ const architecture = cpuTypes[header.readUInt32LE(4)];
1014
+ return architecture ? [architecture] : undefined;
1015
+ }
1016
+ return undefined;
1017
+ }
1018
+ //# sourceMappingURL=unity-installer.js.map