|
| 1 | +#!/usr/bin/env node |
| 2 | + |
| 3 | +import 'source-map-support/register'; |
| 4 | +import * as fs from 'fs'; |
| 5 | +import * as os from 'os'; |
| 6 | +import { Command } from 'commander'; |
| 7 | +import path, { join } from 'path'; |
| 8 | +import { LicenseType, LicensingClient } from './license-client'; |
| 9 | +import { PromptForSecretInput } from './utilities'; |
| 10 | +import { UnityHub } from './unity-hub'; |
| 11 | +import { Logger, LogLevel } from './logging'; |
| 12 | +import { UnityVersion } from './unity-version'; |
| 13 | +import { UnityProject } from './unity-project'; |
| 14 | +import { CheckAndroidSdkInstalled } from './android-sdk'; |
| 15 | +import { UnityEditor } from './unity-editor'; |
| 16 | + |
| 17 | +const pkgPath = join(__dirname, '..', 'package.json'); |
| 18 | +const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); |
| 19 | +const program = new Command(); |
| 20 | + |
| 21 | +program.name('unity-cli') |
| 22 | + .description('A command line utility for the Unity Game Engine.') |
| 23 | + .version(pkg.version); |
| 24 | + |
| 25 | +program.command('license-version') |
| 26 | + .description('Print the version of the Unity License Client.') |
| 27 | + .action(async () => { |
| 28 | + const client = new LicensingClient(); |
| 29 | + await client.Version(); |
| 30 | + }); |
| 31 | + |
| 32 | +program.command('activate-license') |
| 33 | + .description('Activate a Unity license.') |
| 34 | + .option('-e, --email <email>', 'Email associated with the Unity account. Required when activating a personal or professional license.') |
| 35 | + .option('-p, --password <password>', 'Password for the Unity account. Required when activating a personal or professional license.') |
| 36 | + .option('-s, --serial <serial>', 'License serial number. Required when activating a professional license.') |
| 37 | + .option('-l, --license <license>', 'License type (personal, professional, floating).') |
| 38 | + .option('-c, --config <config>', 'Path to the configuration file. Required when activating a floating license.') |
| 39 | + .option('--verbose', 'Enable verbose logging.') |
| 40 | + .action(async (options) => { |
| 41 | + if (options.verbose) { |
| 42 | + Logger.instance.logLevel = LogLevel.DEBUG; |
| 43 | + } |
| 44 | + |
| 45 | + Logger.instance.debug(JSON.stringify(options)); |
| 46 | + |
| 47 | + const client = new LicensingClient(); |
| 48 | + const licenseStr: string = options.license?.toString()?.trim(); |
| 49 | + |
| 50 | + if (!licenseStr || licenseStr.length === 0) { |
| 51 | + throw new Error('License type is required. Use -l or --license to specify it.'); |
| 52 | + } |
| 53 | + |
| 54 | + const licenseType: LicenseType = options.license.toLowerCase() as LicenseType; |
| 55 | + |
| 56 | + if (![LicenseType.personal, LicenseType.professional, LicenseType.floating].includes(licenseType)) { |
| 57 | + throw new Error(`Invalid license type: ${licenseType}`); |
| 58 | + } |
| 59 | + |
| 60 | + if (licenseType !== LicenseType.floating) { |
| 61 | + if (!options.email) { |
| 62 | + options.email = await PromptForSecretInput('Email: '); |
| 63 | + } |
| 64 | + |
| 65 | + if (!options.password) { |
| 66 | + options.password = await PromptForSecretInput('Password: '); |
| 67 | + } |
| 68 | + |
| 69 | + if (licenseType === LicenseType.professional && !options.serial) { |
| 70 | + options.serial = await PromptForSecretInput('Serial: '); |
| 71 | + } |
| 72 | + } |
| 73 | + |
| 74 | + await client.Activate(licenseType, options.config, options.serial, options.email, options.password); |
| 75 | + }); |
| 76 | + |
| 77 | +program.command('return-license') |
| 78 | + .description('Return a Unity license.') |
| 79 | + .option('-l, --license <license>', 'License type (personal, professional, floating)') |
| 80 | + .option('--verbose', 'Enable verbose logging.') |
| 81 | + .action(async (options) => { |
| 82 | + if (options.verbose) { |
| 83 | + Logger.instance.logLevel = LogLevel.DEBUG; |
| 84 | + } |
| 85 | + |
| 86 | + Logger.instance.debug(JSON.stringify(options)); |
| 87 | + |
| 88 | + const client = new LicensingClient(); |
| 89 | + const licenseStr: string = options.license?.toString()?.trim(); |
| 90 | + |
| 91 | + if (!licenseStr || licenseStr.length === 0) { |
| 92 | + throw new Error('License type is required. Use -l or --license to specify it.'); |
| 93 | + } |
| 94 | + |
| 95 | + const licenseType: LicenseType = licenseStr.toLowerCase() as LicenseType; |
| 96 | + |
| 97 | + if (![LicenseType.personal, LicenseType.professional, LicenseType.floating].includes(licenseType)) { |
| 98 | + throw new Error(`Invalid license type: ${licenseType}`); |
| 99 | + } |
| 100 | + |
| 101 | + await client.Deactivate(licenseType); |
| 102 | + }); |
| 103 | + |
| 104 | +program.command('hub-version') |
| 105 | + .description('Print the version of the Unity Hub.') |
| 106 | + .action(async () => { |
| 107 | + const unityHub = new UnityHub(); |
| 108 | + const version = await unityHub.Version(); |
| 109 | + process.stdout.write(`${version}\n`); |
| 110 | + }); |
| 111 | + |
| 112 | +program.command('hub-install') |
| 113 | + .description('Install the Unity Hub.') |
| 114 | + .option('--verbose', 'Enable verbose logging.') |
| 115 | + .option('--json', 'Prints the last line of output as JSON string.') |
| 116 | + .action(async (options) => { |
| 117 | + if (options.verbose) { |
| 118 | + Logger.instance.logLevel = LogLevel.DEBUG; |
| 119 | + } |
| 120 | + |
| 121 | + const unityHub = new UnityHub(); |
| 122 | + const hubPath = await unityHub.Install(); |
| 123 | + |
| 124 | + if (options.json) { |
| 125 | + process.stdout.write(`\n${JSON.stringify({ UNITY_HUB_PATH: hubPath })}\n`); |
| 126 | + } else { |
| 127 | + process.stdout.write(`${hubPath}\n`); |
| 128 | + } |
| 129 | + }); |
| 130 | + |
| 131 | +program.command('hub-path') |
| 132 | + .description('Print the path to the Unity Hub executable.') |
| 133 | + .option('--json', 'Prints the last line of output as JSON string.') |
| 134 | + .action(async (options) => { |
| 135 | + const hub = new UnityHub(); |
| 136 | + if (options.json) { |
| 137 | + process.stdout.write(`\n${JSON.stringify({ UNITY_HUB_PATH: hub.executable })}\n`); |
| 138 | + } else { |
| 139 | + process.stdout.write(`${hub.executable}\n`); |
| 140 | + } |
| 141 | + }); |
| 142 | + |
| 143 | +program.command('hub') |
| 144 | + .description('Run commands directly to the Unity Hub. (You need not to pass --headless or -- to this command).') |
| 145 | + .allowUnknownOption(true) |
| 146 | + .argument('<args...>', 'Arguments to pass to the Unity Hub executable.') |
| 147 | + .option('--verbose', 'Enable verbose logging.') |
| 148 | + .action(async (args: string[], options) => { |
| 149 | + if (options.verbose) { |
| 150 | + Logger.instance.logLevel = LogLevel.DEBUG; |
| 151 | + } |
| 152 | + |
| 153 | + Logger.instance.debug(JSON.stringify({ args, options })); |
| 154 | + |
| 155 | + const unityHub = new UnityHub(); |
| 156 | + await unityHub.Exec(args, { silent: false, showCommand: Logger.instance.logLevel === LogLevel.DEBUG }); |
| 157 | + }); |
| 158 | + |
| 159 | +program.command('setup-unity') |
| 160 | + .description('Sets up the environment for the specified project and finds or installs the Unity Editor version for it.') |
| 161 | + .option('-p, --unity-project <unityProjectPath>', 'The path to a Unity project or "none" to skip project detection.') |
| 162 | + .option('-u, --unity-version <unityVersion>', 'The Unity version to get (e.g. 2020.3.1f1, 2021.x, 2022.1.*, 6000). If specified, it will override the version read from the project.') |
| 163 | + .option('-c, --changeset <changeset>', 'The Unity changeset to get (e.g. 1234567890ab).') |
| 164 | + .option('-a, --arch <architecture>', 'The Unity architecture to get (e.g. x86_64, arm64). Defaults to the architecture of the current process.') |
| 165 | + .option('-b, --build-targets <buildTargets>', 'The Unity build target to get (e.g. iOS, Android).') |
| 166 | + .option('-m, --modules <modules>', 'The Unity module to get (e.g. ios, android).') |
| 167 | + .option('-i, --install-path <installPath>', 'The path to install the Unity Editor to. By default, it will be installed to the default Unity Hub location.') |
| 168 | + .option('--verbose', 'Enable verbose logging.') |
| 169 | + .option('--json', 'Prints the last line of output as JSON string.') |
| 170 | + .action(async (options) => { |
| 171 | + if (options.verbose) { |
| 172 | + Logger.instance.logLevel = LogLevel.DEBUG; |
| 173 | + } |
| 174 | + |
| 175 | + Logger.instance.debug(JSON.stringify(options)); |
| 176 | + |
| 177 | + let unityProject: UnityProject | undefined; |
| 178 | + |
| 179 | + if (options.unityProject) { |
| 180 | + unityProject = await UnityProject.GetProject(options.unityProject); |
| 181 | + } |
| 182 | + |
| 183 | + if (!options.unityVersion && !unityProject) { |
| 184 | + throw new Error('You must specify a Unity version or project with -u, --unity-version, -p, --unity-project.'); |
| 185 | + } |
| 186 | + |
| 187 | + const unityVersion = unityProject?.version ?? new UnityVersion(options.unityVersion, options.changeset); |
| 188 | + const modules: string[] = options.modules ? options.modules.split(/[ ,]+/).filter(Boolean) : []; |
| 189 | + const buildTargets: string[] = options.buildTargets ? options.buildTargets.split(/[ ,]+/).filter(Boolean) : []; |
| 190 | + const moduleBuildTargetMap = UnityHub.GetPlatformTargetModuleMap(); |
| 191 | + |
| 192 | + for (const target of buildTargets) { |
| 193 | + const module = moduleBuildTargetMap[target]; |
| 194 | + |
| 195 | + if (module === undefined) { |
| 196 | + if (target.toLowerCase() !== 'none') { |
| 197 | + Logger.instance.warn(`${target} is not a valid build target for ${os.type()}`); |
| 198 | + } |
| 199 | + |
| 200 | + continue; |
| 201 | + } |
| 202 | + |
| 203 | + if (!modules.includes(module)) { |
| 204 | + modules.push(module); |
| 205 | + } |
| 206 | + } |
| 207 | + |
| 208 | + if (modules.includes('none') || |
| 209 | + modules.includes('None')) { |
| 210 | + modules.length = 0; |
| 211 | + } |
| 212 | + |
| 213 | + const unityHub = new UnityHub(); |
| 214 | + const editorPath = await unityHub.GetEditor(unityVersion, modules); |
| 215 | + const output: { [key: string]: string } = { |
| 216 | + 'UNITY_HUB_PATH': unityHub.executable, |
| 217 | + 'UNITY_EDITOR': editorPath |
| 218 | + }; |
| 219 | + |
| 220 | + if (unityProject) { |
| 221 | + output['UNITY_PROJECT_PATH'] = unityProject.projectPath; |
| 222 | + |
| 223 | + if (modules.includes('android')) { |
| 224 | + await CheckAndroidSdkInstalled(editorPath, unityProject.projectPath); |
| 225 | + } |
| 226 | + } |
| 227 | + |
| 228 | + if (options.json) { |
| 229 | + process.stdout.write(`\n${JSON.stringify(output)}\n`); |
| 230 | + } |
| 231 | + }); |
| 232 | + |
| 233 | +program.command('create-project') |
| 234 | + .description('Create a new Unity project.') |
| 235 | + .option('--name <projectName>', 'The name of the new Unity project. If unspecified, the project will be created in the specified path or the current working directory.') |
| 236 | + .option('--path <projectPath>', 'The path to create the new Unity project. If unspecified, the current working directory will be used.') |
| 237 | + .option('--template <projectTemplate>', 'The name of the template package to use for creating the unity project. Supports regex patterns.', 'com.unity.template.3d(-cross-platform)?') |
| 238 | + .option('--unity-editor <unityEditorPath>', 'The path to the Unity Editor executable. If unspecified, the UNITY_EDITOR environment variable must be set.') |
| 239 | + .option('--verbose', 'Enable verbose logging.') |
| 240 | + .option('--json', 'Prints the last line of output as JSON string.') |
| 241 | + .action(async (options) => { |
| 242 | + if (options.verbose) { |
| 243 | + Logger.instance.logLevel = LogLevel.DEBUG; |
| 244 | + } |
| 245 | + |
| 246 | + Logger.instance.debug(JSON.stringify(options)); |
| 247 | + |
| 248 | + const editorPath = options.unityEditor?.toString()?.trim() || process.env.UNITY_EDITOR; |
| 249 | + |
| 250 | + if (!editorPath || editorPath.length === 0) { |
| 251 | + throw new Error('The Unity Editor path was not specified. Use -e or --unity-editor to specify it, or set the UNITY_EDITOR environment variable.'); |
| 252 | + } |
| 253 | + |
| 254 | + const unityEditor = new UnityEditor(editorPath); |
| 255 | + |
| 256 | + if (!options.template || options.template.length === 0) { |
| 257 | + throw new Error('The project template name was not specified. Use -t or --template to specify it.'); |
| 258 | + } |
| 259 | + |
| 260 | + const templatePath = unityEditor.GetTemplatePath(options.template); |
| 261 | + const projectName = options.name?.toString()?.trim(); |
| 262 | + |
| 263 | + let projectPath = options.path?.toString()?.trim() || process.cwd(); |
| 264 | + |
| 265 | + if (projectName && projectName.length > 0) { |
| 266 | + projectPath = path.join(projectPath, projectName); |
| 267 | + } |
| 268 | + |
| 269 | + await unityEditor.Run({ |
| 270 | + projectPath: projectPath, |
| 271 | + args: [ |
| 272 | + '-quit', |
| 273 | + '-nographics', |
| 274 | + '-batchmode', |
| 275 | + '-createProject', projectPath, |
| 276 | + '-cloneFromTemplate', templatePath |
| 277 | + ] |
| 278 | + }); |
| 279 | + |
| 280 | + if (options.json) { |
| 281 | + process.stdout.write(`\n${JSON.stringify({ UNITY_PROJECT_PATH: projectPath })}\n`); |
| 282 | + } |
| 283 | + }); |
| 284 | + |
| 285 | +program.command('run') |
| 286 | + .description('Run command line args directly to the Unity Editor.') |
| 287 | + .option('--unity-editor <unityEditorPath>', 'The path to the Unity Editor executable. If unspecified, the UNITY_EDITOR environment variable must be set.') |
| 288 | + .option('--unity-project <unityProjectPath>', 'The path to a Unity project. If unspecified, the UNITY_PROJECT_PATH environment variable or the current working directory will be used.') |
| 289 | + .option('--log-name <logName>', 'The name of the log file.') |
| 290 | + .allowUnknownOption(true) |
| 291 | + .argument('<args...>', 'Arguments to pass to the Unity Editor executable.') |
| 292 | + .option('--verbose', 'Enable verbose logging.') |
| 293 | + .action(async (args: string[], options) => { |
| 294 | + if (options.verbose) { |
| 295 | + Logger.instance.logLevel = LogLevel.DEBUG; |
| 296 | + } |
| 297 | + |
| 298 | + Logger.instance.debug(JSON.stringify({ options, args })); |
| 299 | + |
| 300 | + const editorPath = options.unityEditor?.toString()?.trim() || process.env.UNITY_EDITOR; |
| 301 | + |
| 302 | + if (!editorPath || editorPath.length === 0) { |
| 303 | + throw new Error('The Unity Editor path was not specified. Use -e or --unity-editor to specify it, or set the UNITY_EDITOR environment variable.'); |
| 304 | + } |
| 305 | + |
| 306 | + const unityEditor = new UnityEditor(editorPath); |
| 307 | + const projectPath = options.unityProject?.toString()?.trim() || process.env.UNITY_PROJECT_PATH || process.cwd(); |
| 308 | + const unityProject = await UnityProject.GetProject(projectPath); |
| 309 | + |
| 310 | + if (!unityProject) { |
| 311 | + throw new Error(`The specified path is not a valid Unity project: ${projectPath}`); |
| 312 | + } |
| 313 | + |
| 314 | + if (!args.includes('-logFile')) { |
| 315 | + const logPath = unityEditor.GenerateLogFilePath(unityProject.projectPath, options.logName); |
| 316 | + args.push('-logFile', logPath); |
| 317 | + } |
| 318 | + |
| 319 | + await unityEditor.Run({ |
| 320 | + args: [...args] |
| 321 | + }); |
| 322 | + }); |
| 323 | + |
| 324 | +program.parse(process.argv); |
0 commit comments