├── .gitignore ├── LICENSE.md ├── README.md ├── SECURITY.md ├── Tests ├── Data │ ├── manifest.json │ └── manifestlike.json ├── E2ETests │ └── Update-UnityPackageManagerConfig.Tests.ps1 ├── README.md ├── UnitTests │ ├── Find-UnitySetupInstaller.Tests.ps1 │ └── Update-UnityPackageManagerConfig.Tests.ps1 ├── e2etests.ps1 ├── testhelpers.psm1 └── unittests.ps1 ├── UnitySetup ├── DSCResources │ ├── xUnityLicense │ │ ├── xUnityLicense.psm1 │ │ └── xUnityLicense.schema.mof │ └── xUnitySetupInstance │ │ ├── xUnitySetupInstance.psm1 │ │ └── xUnitySetupInstance.schema.mof ├── Examples │ ├── Sample_xUnity.ps1 │ ├── Sample_xUnity_Install.ps1 │ └── Sample_xUnity_Uninstall.ps1 ├── UnitySetup.psd1 └── UnitySetup.psm1 ├── appveyor.yml └── build.ps1 /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | /Tests/Test-Results.xml -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unity Setup Powershell Module 2 | 3 | This PowerShell module contains tools for managing and automating your Unity installs and projects. 4 | 5 | # Table of Contents 6 | 7 | 1. [Unity Setup Powershell Module](#unity-setup-powershell-module) 8 | 2. [Builds](#builds) 9 | 3. [Installation](#installation) 10 | 4. [Using](#using) 11 | 5. [Feedback](#feedback) 12 | 6. [Contributing](#contributing) 13 | 7. [Testing](#testing) 14 | 8. [Reporting Security Issues](#reporting-security-issues) 15 | 16 | 17 | ## Builds 18 | 19 | ### Master 20 | [![Build status](https://ci.appveyor.com/api/projects/status/m7ykg9s8gw23fn6h/branch/master?svg=true)](https://ci.appveyor.com/project/jwittner/unitysetup-powershell/branch/master) 21 | 22 | The `master` branch is automatically built and deployed to the [PowerShell Gallery](https://www.powershellgallery.com/packages/UnitySetup). 23 | 24 | ### Develop 25 | [![Build status](https://ci.appveyor.com/api/projects/status/m7ykg9s8gw23fn6h/branch/develop?svg=true)](https://ci.appveyor.com/project/jwittner/unitysetup-powershell/branch/develop) 26 | 27 | The `develop` branch is automatically built and deployed as a prerelease module to the [PowerShell Gallery](https://www.powershellgallery.com/packages/UnitySetup). 28 | 29 | ## Installation 30 | 31 | ```powershell 32 | Install-Module UnitySetup -Scope CurrentUser 33 | ``` 34 | 35 | ## Using 36 | 37 | ### Cmdlets 38 | Find all of your Unity installs: 39 | ```powershell 40 | Get-UnitySetupInstance 41 | 42 | # Example output: 43 | # Version Components Path 44 | # ------- ---------- ---- 45 | # 2017.1.2f1 Windows, UWP, UWP_IL2CPP C:\Program Files\Unity-2017.1.2f1\ 46 | # 2017.1.3f1 Windows, UWP, UWP_IL2CPP C:\Program Files\Unity-2017.1.3f1\ 47 | # 2017.2.1f1 Windows, UWP, UWP_IL2CPP C:\Program Files\Unity-2017.2.1f1\ 48 | # 2017.3.1f1 Windows, UWP, UWP_IL2CPP, Linux, Vuforia C:\Program Files\Unity-2017.3.1f1\ 49 | # 2018.1.0b4 Windows, UWP, UWP_IL2CPP, Vuforia C:\Program Files\Unity-2018.1.0b4\ 50 | # 2018.1.0b8 All C:\Program Files\Unity-2018.1.0b8\ 51 | # 2017.1.0p5 Windows, UWP, UWP_IL2CPP C:\Program Files\Unity.2017.1.0p5\ 52 | # 2017.1.1f1 Windows, UWP, UWP_IL2CPP C:\Program Files\Unity.2017.1.1f1\ 53 | # 2017.1.1p3 Windows, StandardAssets, UWP, UWP_IL2CPP C:\Program Files\Unity.2017.1.1p3\ 54 | # 2017.2.0f3 Windows, UWP, UWP_IL2CPP, Vuforia C:\Program Files\Unity.2017.2.0f3\ 55 | # 2017.3.0f3 Windows, UWP, UWP_IL2CPP, Mac, Vuforia C:\Program Files\Unity.2017.3.0f3\ 56 | ``` 57 | Select the Unity installs that you want: 58 | ```powershell 59 | Get-UnitySetupInstance | Select-UnitySetupInstance -Latest 60 | Get-UnitySetupInstance | Select-UnitySetupInstance -Version '2017.1.1f1' 61 | Get-UnitySetupInstance | Select-UnitySetupInstance -Project '.\MyUnityProject' 62 | ``` 63 | Find all the Unity projects recursively: 64 | ```powershell 65 | Get-UnityProjectInstance -Recurse 66 | 67 | # Example output: 68 | # Version Path ProductName 69 | # ------- ---- ----------- 70 | # 2017.2.0f3 C:\Projects\Project1\OneUnity\ Contoso 71 | # 2017.3.0f3 C:\Projects\Project1\TwoUnity\ Northwind 72 | # 2017.1.1p1 C:\Projects\Project2\ My Cool App 73 | # 2017.1.2f1 C:\Projects\Project3\App.Unity\ TemplateProject 74 | ``` 75 | Launch the right Unity editor for a project: 76 | ```powershell 77 | Start-UnityEditor 78 | Start-UnityEditor -Project .\MyUnityProject 79 | Start-UnityEditor -Project .\MyUnityProject -Latest 80 | Start-UnityEditor -Project .\MyUnityProject -Version '2017.3.0f3' 81 | ``` 82 | 83 | Using the [Unity Accelerator](https://docs.unity3d.com/2019.3/Documentation/Manual/UnityAccelerator.html): 84 | ```powershell 85 | Start-UnityEditor -Project .\MyUnityProject -CacheServerEndpoint 192.168.0.23 86 | Start-UnityEditor -Project .\MyUnityProject -CacheServerEndpoint 192.168.0.23:2523 -CacheServerNamespacePrefix "dev" 87 | Start-UnityEditor -Project .\MyUnityProject -CacheServerEndpoint 192.168.0.23 -CacheServerNamespacePrefix "dev" -CacheServerDisableDownload 88 | Start-UnityEditor -Project .\MyUnityProject -CacheServerEndpoint 192.168.0.23 -CacheServerDisableUpload 89 | ``` 90 | Launch many projects at the same time: 91 | ```powershell 92 | Get-UnityProjectInstance -Recurse | Start-UnityEditor 93 | ``` 94 | Invoke methods with arbitrary arguments: 95 | ```powershell 96 | Start-UnityEditor -ExecuteMethod Build.Invoke -BatchMode -Quit -LogFile .\build.log -Wait -AdditionalArguments "-BuildArg1 -BuildArg2" 97 | ``` 98 | Test the meta file integrity of Unity Projects: 99 | ```powershell 100 | Test-UnityProjectInstanceMetaFileIntegrity # Test project in current folder 101 | Test-UnityProjectInstanceMetaFileIntegrity .\MyUnityProject 102 | Test-UnityProjectInstanceMetaFileIntegrity -Project .\MyUnityProject 103 | Get-UnityProjectInstance -Recurse | Test-UnityProjectInstanceMetaFileIntegrity 104 | 105 | # Example output: 106 | # True 107 | ``` 108 | Get meta file integrity issues for Unity Projects: 109 | ```powershell 110 | Test-UnityProjectInstanceMetaFileIntegrity .\MyUnityProject -PassThru 111 | 112 | # Example output: 113 | # Item Issue 114 | # ---- ----- 115 | # C:\MyUnityProject\Assets\SomeFolder Directory is missing associated meta file. 116 | # C:\MyUnityProject\Assets\SomeFolder\SomeShader.shader File is missing associated meta file. 117 | # C:\MyUnityProject\Assets\SomeFolder\SomeOtherShader.shader.meta Meta file is missing associated item. 118 | # C:\MyUnityProject\Assets\SomeFolder\SomeNewShader.shader.meta Meta file guid collision with C:\MyUnityProject\Assets\SomeFolder\SomeOtherShader.shader.meta 119 | ``` 120 | 121 | Find the installers for a particular version: 122 | ```powershell 123 | Find-UnitySetupInstaller -Version '2017.3.0f3' | Format-Table 124 | 125 | # Example output: 126 | # ComponentType Version Length LastModified DownloadUrl 127 | # ------------- ------- ------ ------------ ----------- 128 | # Windows 2017.3.0f3 553688024 12/18/2017 8:05:31 AM https://download.unity3d.com/download_unity/... 129 | # Linux 2017.3.0f3 122271984 12/18/2017 8:06:53 AM https://download.unity3d.com/download_unity/... 130 | # Mac 2017.3.0f3 28103888 12/18/2017 8:06:53 AM https://download.unity3d.com/download_unity/... 131 | # Documentation 2017.3.0f3 358911256 12/18/2017 8:07:34 AM https://download.unity3d.com/download_unity/... 132 | # StandardAssets 2017.3.0f3 189886032 12/18/2017 8:05:50 AM https://download.unity3d.com/download_unity/... 133 | # UWP 2017.3.0f3 172298008 12/18/2017 8:07:04 AM https://download.unity3d.com/download_unity/... 134 | # UWP_IL2CPP 2017.3.0f3 152933480 12/18/2017 8:07:10 AM https://download.unity3d.com/download_unity/... 135 | # Android 2017.3.0f3 194240888 12/18/2017 8:05:58 AM https://download.unity3d.com/download_unity/... 136 | # iOS 2017.3.0f3 802853872 12/18/2017 8:06:46 AM https://download.unity3d.com/download_unity/... 137 | # AppleTV 2017.3.0f3 273433528 12/18/2017 8:06:09 AM https://download.unity3d.com/download_unity/... 138 | # Facebook 2017.3.0f3 32131560 12/18/2017 8:06:12 AM https://download.unity3d.com/download_unity/... 139 | # Vuforia 2017.3.0f3 65677296 12/18/2017 8:07:12 AM https://download.unity3d.com/download_unity/... 140 | # WebGL 2017.3.0f3 134133288 12/18/2017 8:07:19 AM https://download.unity3d.com/download_unity/... 141 | ``` 142 | 143 | Limit what components you search for: 144 | ```powershell 145 | Find-UnitySetupInstaller -Version 2017.3.0f3 -Components 'Windows','Documentation' | Format-Table 146 | 147 | # Example output: 148 | # ComponentType Version Length LastModified DownloadUrl 149 | # ------------- ------- ------ ------------ ----------- 150 | # Windows 2017.3.0f3 553688024 12/18/2017 8:05:31 AM https://download.unity3d.com/download_unity/... 151 | # Documentation 2017.3.0f3 358911256 12/18/2017 8:07:34 AM https://download.unity3d.com/download_unity/... 152 | ``` 153 | 154 | Install UnitySetup instances: 155 | ```powershell 156 | # Pipeline is supported, but downloads, then installs, then downloads, etc. 157 | Find-UnitySetupInstaller -Version '2017.3.0f3' | Install-UnitySetupInstance 158 | 159 | # This will issue all downloads together, then install each. 160 | Install-UnitySetupInstance -Installers (Find-UnitySetupInstaller -Version '2017.3.0f3') 161 | ``` 162 | 163 | Manage Unity licenses. 164 | ```powershell 165 | # Get any active licenses 166 | Get-UnityLicense 167 | 168 | # Example Output: 169 | # LicenseVersion : 6.x 170 | # Serial : System.Security.SecureString 171 | # UnityVersion : 2017.4.2f2 172 | # DisplaySerial : AB-CDEF-GHIJ-KLMN-OPQR-XXXX 173 | # ActivationDate : 2017-07-13 16:32:16 174 | # StartDate : 2017-07-12 00:00:00 175 | # StopDate : 2019-01-01 00:00:00 176 | # UpdateDate : 2018-05-11 23:47:10 177 | 178 | # Activate a license 179 | Start-UnityEditor -Credential -Serial -Wait 180 | 181 | # Return license 182 | Start-UnityEditor -Credential -ReturnLicense -Wait 183 | ``` 184 | 185 | Manage Unity Package Manager configuration 186 | ``` powershell 187 | # Update NPM auth tokens for my project manifest 188 | Update-UnityPackageManagerConfig -ProjectManifestPath "C:\MyUnityProject\Packages\manifest.json" 189 | 190 | # Example output 191 | # A Personal Access Token (PAT) will be created for you with the following details 192 | # Name: myorg_Package-Read (Automated) 193 | # Organization: myorg 194 | # Expiration: 2024-07-17T13:53:04.889Z 195 | # Would you like to continue? (Default: y): y 196 | # 197 | # ScopedURL Auth 198 | # --------- ---- 199 | # https://pkgs.dev.azure.com/myorg/myproject/_packaging/MyRegistry/npm/registry my_auth_token_string_base64= 200 | ``` 201 | 202 | ### DSC 203 | UnitySetup includes the xUnitySetupInstance DSC Resource. An example configuration might look like: 204 | 205 | ```powershell 206 | <# 207 | Install multiple versions of Unity and several components 208 | #> 209 | Configuration Sample_xUnitySetupInstance_Install { 210 | param( 211 | [PSCredential]$UnityCredential, 212 | [PSCredential]$UnitySerial 213 | ) 214 | 215 | Import-DscResource -ModuleName UnitySetup 216 | 217 | Node 'localhost' { 218 | 219 | xUnitySetupInstance Unity { 220 | Versions = '2017.4.2f2,2018.1.0f2' 221 | Components = 'Windows', 'Mac', 'Linux', 'UWP', 'iOS' 222 | Ensure = 'Present' 223 | } 224 | 225 | xUnityLicense UnityLicense { 226 | Name = 'UL01' 227 | Credential = $UnityCredential 228 | Serial = $UnitySerial 229 | Ensure = 'Present' 230 | UnityVersion = '2017.4.2f2' 231 | DependsOn = '[xUnitySetupInstance]Unity' 232 | } 233 | } 234 | } 235 | ``` 236 | 237 | See more by perusing the `UnitySetup\Examples` folder. 238 | 239 | # Feedback 240 | To file issues or suggestions, please use the [Issues](https://github.com/Microsoft/unitysetup.powershell/issues) page for this project on GitHub. 241 | 242 | 243 | # Contributing 244 | 245 | This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit https://cla.microsoft.com. 246 | 247 | When you submit a pull request, a CLA-bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA. 248 | 249 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 250 | 251 | 252 | ## Testing 253 | 254 | This project includes Pester test runners for both unit tests and end to end tests - which can test against real Unity projects via environment variables. 255 | 256 | To learn more about how to run and write tests, please refer to the [Test Runner Guide](./Tests/README.md). 257 | 258 | 259 | # Reporting Security Issues 260 | 261 | Security issues and bugs should be reported privately, via email, to the Microsoft Security Response Center (MSRC) at [secure@microsoft.com](mailto:secure@microsoft.com). You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Further information, including the [MSRC PGP](https://technet.microsoft.com/en-us/security/dn606155) key, can be found in the [Security TechCenter](https://technet.microsoft.com/en-us/security/default). -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /Tests/Data/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "scopedRegistries": [ 3 | { 4 | "name": "TestRegistry", 5 | "url": "https://placeholder/url", 6 | "scopes": [ 7 | "com.azure", 8 | "com.microsoft", 9 | "org.nuget" 10 | ] 11 | } 12 | ], 13 | "dependencies": { 14 | "com.example.packageA": "2.3.4", 15 | "org.sample.libB": "5.6.7", 16 | "net.test.pluginC": "0.9.1" 17 | }, 18 | "testables": [ 19 | "com.example.testA", 20 | "org.sample.testB" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /Tests/Data/manifestlike.json: -------------------------------------------------------------------------------- 1 | { 2 | "scopedRegistries": [ 3 | { 4 | "name": "TestRegistry", 5 | "url": "https://placeholder/url", 6 | "scopes": [ 7 | "com.azure", 8 | "com.microsoft", 9 | "org.nuget" 10 | ] 11 | } 12 | ], 13 | "zooAnimals": { 14 | "lion": "Panthera leo", 15 | "elephant": "Loxodonta africana", 16 | "zebra": "Equus quagga" 17 | }, 18 | "favoritePrimeNumbers": [ 19 | 2, 20 | 3, 21 | 5, 22 | 7, 23 | 11, 24 | 13 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /Tests/E2ETests/Update-UnityPackageManagerConfig.Tests.ps1: -------------------------------------------------------------------------------- 1 | Describe 'Update-UnityPackageManagerConfig' { 2 | BeforeAll { 3 | Import-Module (Join-Path $PSScriptRoot '..\..\UnitySetup\UnitySetup.psd1') -Force 4 | } 5 | 6 | Context 'E2E Validation of manifest/folder targets' { 7 | It 'supports a root folder target' { 8 | { Update-UnityPackageManagerConfig -SearchPath $env:TEST_UNITY_FOLDERPATH -SearchDepth 5 } | Should -Not -Throw 9 | } 10 | 11 | It 'supports a single manifest target' { 12 | { Update-UnityPackageManagerConfig -ProjectManifestPath $env:TEST_UNITY_MANIFESTPATH } | Should -Not -Throw 13 | } 14 | 15 | It 'supports a search target with multiple manifests' { 16 | { Update-UnityPackageManagerConfig -SearchPath $env:TEST_UNITY_MULTIFOLDERPATH -SearchDepth 5 } | Should -Not -Throw 17 | } 18 | 19 | It 'supports a single manifest-like target (any JSON file with valid scoped registries)' { 20 | { Update-UnityPackageManagerConfig -ProjectManifestPath $env:TEST_UNITY_MANIFESTLIKEPATH } | Should -Not -Throw 21 | } 22 | 23 | It 'should throw if manifest path is a folder, not a file' { 24 | { Update-UnityPackageManagerConfig -ProjectManifestPath $env:TEST_UNITY_FOLDERPATH } | Should -Throw "* is not a valid file" 25 | } 26 | } 27 | 28 | Context 'E2E Validation of AzureSubscriptionID options' { 29 | It 'should throw on malformed AzureSubscription guid' { 30 | { Update-UnityPackageManagerConfig -ProjectManifestPath $env:TEST_UNITY_MANIFESTPATH -AzureSubscription "abcd" } | Should -Throw "*Unrecognized Guid format*" 31 | } 32 | 33 | It 'should accept a valid AzureSubscription guid' { 34 | { Update-UnityPackageManagerConfig -ProjectManifestPath $env:TEST_UNITY_MANIFESTPATH -AzureSubscription $env:TEST_AZURESUBSCRIPTION_ID } | Should -Not -Throw 35 | } 36 | } 37 | 38 | Context 'E2E Validation of AutoClean parameter' { 39 | It 'should run with AutoClean enabled' { 40 | { Update-UnityPackageManagerConfig -ProjectManifestPath $env:TEST_UNITY_MANIFESTPATH -AutoClean } | Should -Not -Throw 41 | } 42 | } 43 | 44 | Context 'E2E Validation of ManualPAT parameter' { 45 | It 'should run with ManualPAT enabled' { 46 | { Update-UnityPackageManagerConfig -ProjectManifestPath $env:TEST_UNITY_MANIFESTPATH -ManualPAT } | Should -Not -Throw 47 | } 48 | } 49 | 50 | Context 'E2E Validation of VerifyOnly parameter' { 51 | It 'should run with VerifyOnly enabled' { 52 | { Update-UnityPackageManagerConfig -ProjectManifestPath $env:TEST_UNITY_MANIFESTPATH -VerifyOnly } | Should -Not -Throw 53 | } 54 | } 55 | 56 | Context 'Combination of parameters' { 57 | It 'should run with AutoClean, ManualPAT, and VerifyOnly enabled' { 58 | { Update-UnityPackageManagerConfig -ProjectManifestPath $env:TEST_UNITY_MANIFESTPATH -AutoClean -ManualPAT -VerifyOnly } | Should -Not -Throw 59 | } 60 | 61 | It 'should run with SearchDepth, PATLifetime, and AzureSubscription parameters' { 62 | { Update-UnityPackageManagerConfig -SearchPath $env:TEST_UNITY_FOLDERPATH -SearchDepth 7 -PATLifetime 30 -AzureSubscription $env:TEST_AZURESUBSCRIPTION_ID } | Should -Not -Throw 63 | } 64 | } 65 | 66 | Context 'Edge cases for parameter values' { 67 | It 'should throw if SearchDepth is negative' { 68 | { Update-UnityPackageManagerConfig -SearchPath $env:TEST_UNITY_FOLDERPATH -SearchDepth -1 } | Should -Throw '*Cannot convert value "-1" to type "System.UInt32"*' 69 | } 70 | 71 | It 'should throw if PATLifetime is zero' { 72 | { Update-UnityPackageManagerConfig -ProjectManifestPath $env:TEST_UNITY_MANIFESTPATH -PATLifetime 0 } | Should -Throw "*PATLifetime must be greater than zero*" 73 | } 74 | 75 | It 'should throw if AzureSubscription is empty GUID' { 76 | { Update-UnityPackageManagerConfig -ProjectManifestPath $env:TEST_UNITY_MANIFESTPATH -AzureSubscription [guid]::Empty } | Should -Throw "*Unrecognized Guid format*" 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Tests/README.md: -------------------------------------------------------------------------------- 1 | # Test Runner Guide 2 | 3 | This document provides an overview of the test runner setup and how to use it for both end-to-end (E2E) and unit testing in this project. 4 | 5 | ## Table of Contents 6 | 7 | 1. [Overview](#overview) 8 | 2. [Environment Setup](#environment-setup) 9 | 3. [Running the Tests](#running-the-tests) 10 | - [End-to-End Tests](#end-to-end-tests) 11 | - [Unit Tests](#unit-tests) 12 | 4. [Folder Structure](#folder-structure) 13 | 5. [Additional Information](#additional-information) 14 | 15 | ## Overview 16 | 17 | The test runner is designed to facilitate both end-to-end and unit testing on real Unity project targets using UnitySetup cmdlets. Including real generation of Unity Package Manager auth tokens. (You may need to delete your .toml file locally for fresh runs) 18 | 19 | ## Environment Setup (End to End Tests) 20 | 21 | Before running the tests, ensure that you have one or more Unity projects locally that can serve for the following environment variables: 22 | 23 | - `TEST_UNITY_FOLDERPATH`: Path to the root folder of a Unity project. 24 | - `TEST_UNITY_MANIFESTPATH`: Path to a valid Unity project manifest. 25 | - `TEST_UNITY_MULTIFOLDERPATH`: Path to the root folder of a Unity project with multiple manifests in subfolders less than 5 directories deep. 26 | - `TEST_UNITY_MANIFESTLIKEPATH`: Path to a valid Unity project manifest-like file. (Any valid JSON file with scoped registries) 27 | - `TEST_AZURESUBSCRIPTION_ID`: Azure Subscription ID required for certain test scenarios. 28 | 29 | These environment variables can be set interactively when running the tests or manually before running the test scripts. 30 | 31 | ## Running the Tests 32 | 33 | ### End-to-End Tests 34 | 35 | The end-to-end tests are located in the `E2ETests` folder. These tests validate the entire workflow, including real environment variables. 36 | 37 | To run the end-to-end tests: 38 | 39 | ```powershell 40 | cd Tests 41 | .\e2etests.ps1 42 | ``` 43 | 44 | This script will import the necessary modules and run all tests in the `E2ETests` folder, using the provided environment variables. 45 | 46 | ### Unit Tests 47 | 48 | The unit tests are located in the `UnitTests` folder. These tests use mocked data and functions to isolate and validate individual components. 49 | 50 | To run the unit tests: 51 | 52 | ```powershell 53 | cd Tests 54 | .\unittests.ps1 55 | ``` 56 | 57 | This script will run all unit tests in the `UnitTests` folder, using mock functions and predefined input/output. 58 | 59 | ## Additional Information 60 | 61 | - The test scripts utilize `Pester` and `PSScriptAnalyzer` modules. Ensure these modules are installed and available in your environment before running the tests. 62 | - Test results can be output to the console or captured programmatically using the `-PassThru` switch. 63 | 64 | For further customization, review the provided test scripts and modify the parameters or mocks as needed. 65 | -------------------------------------------------------------------------------- /Tests/UnitTests/Find-UnitySetupInstaller.Tests.ps1: -------------------------------------------------------------------------------- 1 | Describe 'Find-UnitySetupInstaller' { 2 | 3 | BeforeEach { 4 | Import-Module "$PSScriptRoot\..\..\UnitySetup\UnitySetup.psd1" -Force 5 | } 6 | 7 | Context 'Input Validation' { 8 | It 'throws on invalidly formatted version' { 9 | { Find-UnitySetupInstaller -Version "" } | Should -Throw "Cannot process argument transformation*" 10 | } 11 | } 12 | 13 | Context 'Function Execution' { 14 | It 'throws on non-existent version' { 15 | { Find-UnitySetupInstaller -Version "1.2.3f1" } | Should -Throw "Could not find archives for Unity version*" 16 | } 17 | 18 | It 'finds an existing version' { 19 | # https://unity.com/releases/editor/whats-new/2022.3.15 lists 13 for Windows, but we don't support Mac_Server 20 | Find-UnitySetupInstaller -Version "2022.3.15f1" -ExplicitOS Windows | Should -HaveCount 12 21 | } 22 | 23 | # TODO: Support Mac ARM as a platform 24 | # It 'does not find VisionOS before supported (Mac)' { 25 | # Find-UnitySetupInstaller -Version "2022.3.15f1" -Components VisionOS -ExplicitOS Mac | Should -HaveCount 0 26 | # } 27 | 28 | # It 'does find VisionOS once supported (Mac)' { 29 | # Find-UnitySetupInstaller -Version "2022.3.18f1" -Components VisionOS -ExplicitOS Mac | Should -HaveCount 1 30 | # } 31 | 32 | It 'does not find VisionOS before supported (Windows)' { 33 | Find-UnitySetupInstaller -Version "2022.3.18f1" -Components VisionOS -ExplicitOS Windows ` 34 | | Should -HaveCount 0 35 | } 36 | 37 | It 'finds VisionOS once supported (Windows, explicit)' { 38 | Find-UnitySetupInstaller -Version "2022.3.21f1" -Components VisionOS -ExplicitOS Windows ` 39 | | Should -HaveCount 1 40 | } 41 | 42 | It 'finds VisionOS once supported (Windows, implicit)' { 43 | Find-UnitySetupInstaller -Version "2022.3.21f1" -ExplicitOS Windows ` 44 | | Where-Object -Property ComponentType -Eq VisionOS ` 45 | | Should -HaveCount 1 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Tests/UnitTests/Update-UnityPackageManagerConfig.Tests.ps1: -------------------------------------------------------------------------------- 1 | Describe 'Update-UnityPackageManagerConfig' { 2 | 3 | BeforeEach { 4 | Import-Module "$PSScriptRoot\..\..\UnitySetup\UnitySetup.psd1" -Force 5 | 6 | Mock -CommandName 'Import-UnityProjectManifest' -MockWith { @() } -ModuleName UnitySetup 7 | Mock -CommandName 'Import-TOMLFile' -MockWith { @() } -ModuleName UnitySetup 8 | Mock -CommandName 'Update-PackageAuthConfig' -MockWith { @() } -ModuleName UnitySetup 9 | Mock -CommandName 'Export-UPMConfig' -MockWith { @() } -ModuleName UnitySetup 10 | Mock -CommandName 'Invoke-WebRequest' -MockWith { 11 | return [pscustomobject]@{ 12 | StatusCode = 200 13 | Content = '{"patToken": {"token": "mockedPATToken"}}' 14 | } 15 | } -ModuleName UnitySetup 16 | Mock -CommandName 'Confirm-PAT' -MockWith { $true } -ModuleName UnitySetup 17 | } 18 | 19 | Context 'Input Validation' { 20 | It 'throws on empty parameters' { 21 | { Update-UnityPackageManagerConfig } | Should -Throw "*insufficient number of parameters were provided." 22 | } 23 | 24 | It 'throws on null ProjectManifestPath' { 25 | { Update-UnityPackageManagerConfig -ProjectManifestPath "" } | Should -Throw "*The argument is null or empty.*" 26 | } 27 | } 28 | 29 | Context 'Function Execution' { 30 | It 'supports a root folder target' { 31 | { Update-UnityPackageManagerConfig -SearchPath "$PSScriptRoot\..\Data" -SearchDepth 5 } | Should -Not -Throw 32 | } 33 | 34 | It 'supports a single manifest target' { 35 | { Update-UnityPackageManagerConfig -ProjectManifestPath "$PSScriptRoot\..\Data\manifest.json" } | Should -Not -Throw 36 | } 37 | 38 | It 'supports a search target with multiple manifests' { 39 | { Update-UnityPackageManagerConfig -SearchPath "$PSScriptRoot\..\Data" -SearchDepth 5 } | Should -Not -Throw 40 | } 41 | 42 | It 'supports a single manifest-like target (any JSON file with valid scoped registries)' { 43 | { Update-UnityPackageManagerConfig -ProjectManifestPath "$PSScriptRoot\..\Data\manifestlike.json" } | Should -Not -Throw 44 | } 45 | 46 | It 'should throw if manifest path is a folder, not a file' { 47 | { Update-UnityPackageManagerConfig -ProjectManifestPath "$PSScriptRoot\..\Data" } | Should -Throw "* is not a valid file" 48 | } 49 | } 50 | 51 | Context 'AzureSubscription Validation' { 52 | It 'should throw on malformed AzureSubscription guid' { 53 | { Update-UnityPackageManagerConfig -ProjectManifestPath "$PSScriptRoot\..\Data\manifest.json" -AzureSubscription "abcd" } | Should -Throw "*Unrecognized Guid format*" 54 | } 55 | 56 | It 'should accept a valid AzureSubscription guid' { 57 | # Random guid input 58 | { Update-UnityPackageManagerConfig -ProjectManifestPath "$PSScriptRoot\..\Data\manifest.json" -AzureSubscription a4e1d2b6-78e4-4c2a-9f73-1f2a5d6e8b1c } | Should -Not -Throw 59 | } 60 | 61 | It 'throws on empty guid for AzureSubscription' { 62 | { Update-UnityPackageManagerConfig -ProjectManifestPath "$PSScriptRoot\..\Data\manifest.json" -AzureSubscription ([guid]::Empty) } | Should -Throw "*Cannot be empty guid.*" 63 | } 64 | } 65 | 66 | Context 'PAT Lifetime Validation' { 67 | It 'throws on PATLifetime less than or equal to zero' { 68 | { Update-UnityPackageManagerConfig -ProjectManifestPath "$PSScriptRoot\..\Data\manifest.json" -PATLifetime 0 } | Should -Throw "*PATLifetime must be greater than zero*" 69 | } 70 | 71 | It 'should throw if PATLifetime is zero' { 72 | { Update-UnityPackageManagerConfig -ProjectManifestPath "$PSScriptRoot\..\Data\manifest.json" -PATLifetime 0 } | Should -Throw "*PATLifetime must be greater than zero*" 73 | } 74 | } 75 | 76 | Context 'Verify Mode' { 77 | It 'runs in VerifyOnly mode without error' { 78 | { Update-UnityPackageManagerConfig -ProjectManifestPath "$PSScriptRoot\..\Data\manifest.json" -VerifyOnly } | Should -Not -Throw 79 | } 80 | 81 | It 'should run with VerifyOnly enabled' { 82 | { Update-UnityPackageManagerConfig -ProjectManifestPath "$PSScriptRoot\..\Data\manifest.json" -VerifyOnly } | Should -Not -Throw 83 | } 84 | } 85 | 86 | Context 'AutoClean Mode' { 87 | It 'runs in AutoClean mode without error' { 88 | { Update-UnityPackageManagerConfig -ProjectManifestPath "$PSScriptRoot\..\Data\manifest.json" -AutoClean } | Should -Not -Throw 89 | } 90 | 91 | It 'should run with AutoClean enabled' { 92 | { Update-UnityPackageManagerConfig -ProjectManifestPath "$PSScriptRoot\..\Data\manifest.json" -AutoClean } | Should -Not -Throw 93 | } 94 | } 95 | 96 | Context 'ManualPAT parameter' { 97 | It 'should run with ManualPAT enabled' { 98 | { Update-UnityPackageManagerConfig -ProjectManifestPath "$PSScriptRoot\..\Data\manifest.json" -ManualPAT } | Should -Not -Throw 99 | } 100 | } 101 | 102 | Context 'Combination of parameters' { 103 | It 'should run with AutoClean, ManualPAT, and VerifyOnly enabled' { 104 | { Update-UnityPackageManagerConfig -ProjectManifestPath "$PSScriptRoot\..\Data\manifest.json" -AutoClean -ManualPAT -VerifyOnly } | Should -Not -Throw 105 | } 106 | 107 | It 'should run with SearchDepth, PATLifetime, and AzureSubscription parameters' { 108 | 109 | # Random guid input 110 | { Update-UnityPackageManagerConfig -SearchPath "$PSScriptRoot\..\Data" -SearchDepth 7 -PATLifetime 30 -AzureSubscription a4e1d2b6-78e4-4c2a-9f73-1f2a5d6e8b1c } | Should -Not -Throw 111 | } 112 | } 113 | 114 | Context 'Edge cases for parameter values' { 115 | It 'should throw if SearchDepth is negative' { 116 | { Update-UnityPackageManagerConfig -SearchPath "$PSScriptRoot\..\Data" -SearchDepth -1 } | Should -Throw '*Cannot convert value "-1" to type "System.UInt32"*' 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Tests/e2etests.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ModuleName = 'PSSCriptAnalyzer'; ModuleVersion = '1.20.0'} 2 | #Requires -Modules @{ModuleName = 'Pester'; ModuleVersion = '5.3.1'} 3 | 4 | [CmdletBinding()] 5 | param([switch]$PassThru) 6 | 7 | # Import the external module 8 | Import-Module "$PSScriptRoot\testhelpers.psm1" -ErrorAction Stop 9 | $modulePath = Join-Path -Path $PSScriptRoot -ChildPath '..\UnitySetup' 10 | $testsFolder = "$PSScriptRoot\E2ETests" 11 | 12 | # Check for the reqired environment variables 13 | if (-not $env:TEST_UNITY_FOLDERPATH) { 14 | $env:TEST_UNITY_FOLDERPATH = Read-Host "Please enter the path for `$env:TEST_UNITY_FOLDERPATH. This should be a path to the root folder of a Unity project" 15 | } 16 | 17 | if (-not $env:TEST_UNITY_MANIFESTPATH) { 18 | $env:TEST_UNITY_MANIFESTPATH = Read-Host "Please enter the path for TEST_UNITY_MANIFESTPATH. This should be a path to a valid Unity project manifest." 19 | } 20 | 21 | if (-not $env:TEST_UNITY_MULTIFOLDERPATH) { 22 | $env:TEST_UNITY_MULTIFOLDERPATH = Read-Host "Please enter the path for TEST_UNITY_MULTIFOLDERPATH. This should be a path to the root folder of a Unity project with multiple manifests in subfolders less than 5 directories deep." 23 | } 24 | 25 | if (-not $env:TEST_UNITY_MANIFESTLIKEPATH) { 26 | $env:TEST_UNITY_MANIFESTLIKEPATH = Read-Host "Please enter the path for TEST_UNITY_MANIFESTPATH. This should be a path to a valid Unity project manifest." 27 | } 28 | 29 | if (-not $env:TEST_AZURESUBSCRIPTION_ID) { 30 | $env:TEST_AZURESUBSCRIPTION_ID = Read-Host "Please enter the value for TEST_AZURESUBSCRIPTION_ID. This should be the azure subscription ID used to reconnect with if an interactive reconnect is required" 31 | } 32 | 33 | 34 | 35 | Write-Host "Running Analyzer..." -ForegroundColor Blue 36 | $analyzerResults = Invoke-Analyzer -ModulePath $modulePath -PassThru:$PassThru 37 | 38 | Write-Host "Running Tests..." -ForegroundColor Blue 39 | $testResults = Invoke-Tests -TargetFolder $testsFolder -PassThru:$PassThru 40 | 41 | if ($PassThru) { 42 | @{ 43 | Analyzer = $analyzerResults 44 | Pester = $testResults 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Tests/testhelpers.psm1: -------------------------------------------------------------------------------- 1 | # testhelpers.psm1 2 | 3 | function Invoke-Analyzer { 4 | param( 5 | [Parameter(Mandatory=$true)] 6 | [ValidateScript({ if ($_.Exists) {return $true } throw "ModulePath $_ must exist"})] 7 | [System.IO.DirectoryInfo]$ModulePath, 8 | [switch]$PassThru 9 | ) 10 | 11 | Import-Module PSScriptAnalyzer -MinimumVersion '1.20.0' -ErrorAction Stop 12 | 13 | $issues = Invoke-ScriptAnalyzer $ModulePath | ForEach-Object { 14 | $_ | Add-Member -PassThru -MemberType ScriptProperty -Name 'DisplayProperties' -Value { ($this | Select-Object RuleName, Severity, ScriptName, Line, Message) } 15 | } 16 | $information = $issues | Where-Object { $_.Severity -eq 'Information' } 17 | $warnings = $issues | Where-Object { $_.Severity -eq 'Warning' } 18 | $errors = $issues | Where-Object { $_.Severity -eq 'Error' } 19 | 20 | foreach ($info in $information) { Write-Verbose $info.DisplayProperties } 21 | foreach ($warn in $warnings) { Write-Warning $warn.DisplayProperties } 22 | foreach ($err in $errors) { Write-Error $err.DisplayProperties } 23 | 24 | if ($PassThru) { 25 | @{ 26 | Issues = $issues 27 | Information = $information 28 | Warnings = $warnings 29 | Errors = $errors 30 | } 31 | } 32 | } 33 | 34 | function Invoke-Tests { 35 | param( 36 | [Parameter(Mandatory=$true)] 37 | [string]$TargetFolder, 38 | [switch]$PassThru 39 | ) 40 | 41 | Import-Module Pester -MinimumVersion '5.3.1' -ErrorAction Stop 42 | $pesterConfig = [PesterConfiguration]::Default 43 | $pesterConfig.TestResult.Enabled = $true 44 | $pesterConfig.TestResult.OutputFormat = 'NUnitXml' 45 | $pesterConfig.TestResult.OutputPath = "Test-Results.xml" 46 | $pesterConfig.Run.Path = $TargetFolder 47 | $pesterConfig.Run.PassThru = $PassThru ? $true : $false 48 | 49 | Invoke-Pester -Configuration $pesterConfig 50 | } 51 | -------------------------------------------------------------------------------- /Tests/unittests.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ModuleName = 'PSSCriptAnalyzer'; ModuleVersion = '1.20.0'} 2 | #Requires -Modules @{ModuleName = 'Pester'; ModuleVersion = '5.3.1'} 3 | 4 | [CmdletBinding()] 5 | param([switch]$PassThru) 6 | 7 | # Import the external module 8 | Import-Module "$PSScriptRoot\testhelpers.psm1" -ErrorAction Stop 9 | $modulePath = Join-Path -Path $PSScriptRoot -ChildPath '..\UnitySetup' 10 | $testsFolder = "$PSScriptRoot\UnitTests" 11 | 12 | 13 | Write-Host "Running Analyzer..." -ForegroundColor Blue 14 | $analyzerResults = Invoke-Analyzer -ModulePath $modulePath -PassThru:$PassThru 15 | 16 | Write-Host "Running Tests..." -ForegroundColor Blue 17 | $testResults = Invoke-Tests -TargetFolder $testsFolder -PassThru:$PassThru 18 | 19 | if ($PassThru) { 20 | @{ 21 | Analyzer = $analyzerResults 22 | Pester = $testResults 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /UnitySetup/DSCResources/xUnityLicense/xUnityLicense.psm1: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | function Get-TargetResource { 4 | [CmdletBinding()] 5 | [OutputType([System.Collections.Hashtable])] 6 | param 7 | ( 8 | [parameter(Mandatory = $true)] 9 | [System.Management.Automation.PSCredential] 10 | $Credential, 11 | 12 | [parameter(Mandatory = $true)] 13 | [System.Management.Automation.PSCredential] 14 | $Serial, 15 | 16 | [parameter(Mandatory = $false)] 17 | [System.String] 18 | $UnityVersion, 19 | 20 | [parameter(Mandatory = $true)] 21 | [System.String] 22 | $Name 23 | ) 24 | 25 | @{ 'Licenses' = (Get-UnityLicense) } 26 | } 27 | 28 | 29 | function Set-TargetResource { 30 | [CmdletBinding()] 31 | param 32 | ( 33 | [parameter(Mandatory = $true)] 34 | [System.Management.Automation.PSCredential] 35 | $Credential, 36 | 37 | [ValidateSet("Present", "Absent")] 38 | [System.String] 39 | $Ensure = 'Present', 40 | 41 | [parameter(Mandatory = $true)] 42 | [System.Management.Automation.PSCredential] 43 | $Serial, 44 | 45 | [System.String] 46 | $UnityVersion, 47 | 48 | [parameter(Mandatory = $true)] 49 | [System.String] 50 | $Name 51 | ) 52 | 53 | if ( Test-TargetResource @PSBoundParameters ) { return } 54 | 55 | $unityArgs = @{ 56 | 'Credential' = $Credential 57 | 'Wait' = $true 58 | } 59 | 60 | if ( $UnityVersion ) { $unityArgs['Version'] = $UnityVersion } 61 | if ( $Ensure -eq 'Present' ) { $unityArgs['Serial'] = $Serial.Password } 62 | else { $unityArgs['ReturnLicense'] = $true } 63 | 64 | Start-UnityEditor @unityArgs -Verbose 65 | } 66 | 67 | 68 | function Test-TargetResource { 69 | [CmdletBinding()] 70 | [OutputType([System.Boolean])] 71 | param 72 | ( 73 | [parameter(Mandatory = $true)] 74 | [System.Management.Automation.PSCredential] 75 | $Credential, 76 | 77 | [ValidateSet("Present", "Absent")] 78 | [System.String] 79 | $Ensure = 'Present', 80 | 81 | [parameter(Mandatory = $true)] 82 | [System.Management.Automation.PSCredential] 83 | $Serial, 84 | 85 | [parameter(Mandatory = $false)] 86 | [System.String] 87 | $UnityVersion, 88 | 89 | [parameter(Mandatory = $true)] 90 | [System.String] 91 | $Name 92 | ) 93 | 94 | foreach ( $license in (Get-UnityLicense -Serial $Serial.Password) ) { 95 | Write-Verbose "Found license: $license" 96 | 97 | $currentSerial = [System.Net.NetworkCredential]::new($null, $license.Serial).Password 98 | $passedSerial = $Serial.GetNetworkCredential().Password 99 | if ( $currentSerial -ne $passedSerial ) { continue } 100 | 101 | return $Ensure -eq 'Present' 102 | } 103 | 104 | return $Ensure -eq 'Absent' 105 | } 106 | 107 | 108 | Export-ModuleMember -Function *-TargetResource 109 | 110 | -------------------------------------------------------------------------------- /UnitySetup/DSCResources/xUnityLicense/xUnityLicense.schema.mof: -------------------------------------------------------------------------------- 1 |  2 | [ClassVersion("1.0.0.0"), FriendlyName("xUnityLicense")] 3 | class xUnityLicense : OMI_BaseResource 4 | { 5 | [Key] string Name; 6 | [Required, EmbeddedInstance("MSFT_Credential")] String Credential; 7 | [Required, EmbeddedInstance("MSFT_Credential")] String Serial; 8 | [Write, ValueMap{"Present","Absent"}, Values{"Present","Absent"}] string Ensure; 9 | [Write] string UnityVersion; 10 | }; 11 | 12 | -------------------------------------------------------------------------------- /UnitySetup/DSCResources/xUnitySetupInstance/xUnitySetupInstance.psm1: -------------------------------------------------------------------------------- 1 | 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. 4 | <# 5 | .Synopsis 6 | Returns the status of Unity installs and the set of their overlapping components. 7 | .Parameter Versions 8 | The versions of Unity to test for. 9 | #> 10 | function Get-TargetResource { 11 | [CmdletBinding()] 12 | [OutputType([System.Collections.Hashtable])] 13 | param 14 | ( 15 | [parameter(Mandatory = $true)] 16 | [System.String] 17 | $Versions 18 | ) 19 | Write-Verbose "Begin executing Get on $Versions" 20 | 21 | [string[]]$splitVersions = $Versions -split ',' | ForEach-Object { $_.Trim() } 22 | 23 | $setupInstances = Get-UnitySetupInstance | Where-Object { $splitVersions -contains $_.Version } 24 | $result = @{ 25 | "Versions" = $setupInstances | Select-Object -ExpandProperty Version | Sort-Object -Unique 26 | "Ensure" = if ($setupInstances.Count -gt 0) { 'Present' } else { 'Absent' } 27 | } 28 | 29 | Write-Verbose "Found versions: $($result['Versions'])" 30 | 31 | if ( $setupInstances.Count -gt 0 ) { 32 | $components = $setupInstances[0].Components; 33 | for ( $i = 1; $i -lt $setupInstances.Count; $i++) { 34 | $components = $components -band $setupInstances[$i].Components; 35 | } 36 | 37 | $result["Components"] = $components 38 | } 39 | 40 | $result 41 | 42 | Write-Verbose "Found overlapping components: $($result['Components'])" 43 | Write-Verbose "End executing Get on $Versions" 44 | } 45 | 46 | <# 47 | .Synopsis 48 | Installs or uninstalls the specified Versions of Unity and corresponding Components. 49 | .Parameter Versions 50 | What versions are we concered with? 51 | .Parameter Ensure 52 | Should we ensure they're there or ensure they're not? 53 | .Parameter Components 54 | What components are we concerned with? 55 | .Notes 56 | Only uninstalls whole versions of Unity. Ensuring components doesn't 57 | mean other components weren't previously installed and aren't still available. 58 | #> 59 | function Set-TargetResource { 60 | [CmdletBinding()] 61 | param 62 | ( 63 | [parameter(Mandatory = $true)] 64 | [System.String] 65 | $Versions, 66 | 67 | [ValidateSet("Present", "Absent")] 68 | [System.String] 69 | $Ensure = 'Present', 70 | 71 | [System.String[]] 72 | $Components = @('All') 73 | ) 74 | 75 | Write-Verbose "Begin executing Set to Ensure $Versions with $Components are $Ensure" 76 | 77 | [string[]]$splitVersions = $Versions -split ',' | ForEach-Object { $_.Trim() } 78 | 79 | switch ($Ensure) { 80 | 'Present' { 81 | foreach ($version in $splitVersions) { 82 | $findArgs = @{ 83 | 'Version' = $version 84 | 'Components' = ConvertTo-UnitySetupComponent $Components -Version $version 85 | } 86 | 87 | $installArgs = @{ 'Cache' = "$env:TEMP\.unitysetup" } 88 | 89 | $setupInstances = Get-UnitySetupInstance | Select-UnitySetupInstance -Version $version 90 | if ($setupInstances.Count -gt 0) { 91 | $findArgs["Components"] = ($findArgs.Components -band (-bnot ($setupInstances[0].Components -band $findArgs.Components))) 92 | $installArgs["Destination"] = $setupInstances[0].Path 93 | } 94 | 95 | # No missing components for this version 96 | if ( $findArgs.Components -eq 0 ) { 97 | Write-Verbose "All components of $version were installed" 98 | continue; 99 | } 100 | 101 | Write-Verbose "Finding $($findArgs["Components"]) installers for $version" 102 | $installArgs["Installers"] = Find-UnitySetupInstaller @findArgs -WarningAction Stop 103 | if ( $installArgs.Installers.Count -gt 0 ) { 104 | Write-Verbose "Starting install of $($installArgs.Installers.Count) components for $version" 105 | Install-UnitySetupInstance @installArgs 106 | Write-Verbose "Finished install of $($installArgs.Installers.Count) components for $version" 107 | } 108 | } 109 | } 110 | 'Absent' { 111 | $setupInstances = Get-UnitySetupInstance | Where-Object { $splitVersions -contains $_.Version } 112 | Write-Verbose "Found $($setupInstances.Count) instance(s) of $splitVersions" 113 | 114 | if ( $setupInstances.Count -gt 0 ) { 115 | Write-Verbose "Starting uninstall of $($setupInstances.Count) versions of Unity" 116 | Uninstall-UnitySetupInstance -Instances $setupInstances 117 | Write-Verbose "Finished uninstall of $($setupInstances.Count) versions of Unity" 118 | } 119 | } 120 | } 121 | 122 | Write-Verbose "End executing Set to Ensure $Versions with $Components are $Ensure" 123 | } 124 | 125 | <# 126 | .Synopsis 127 | Test if the Unity installs are in the desired state. 128 | .Parameter Versions 129 | What versions are we concered with? 130 | .Parameter Ensure 131 | Should we ensure they're there or ensure they're not? 132 | .Parameter Components 133 | What components are we concerned with? 134 | .Notes 135 | This test is not strict. Versions and Components not described are not considered. 136 | #> 137 | function Test-TargetResource { 138 | [CmdletBinding()] 139 | [OutputType([System.Boolean])] 140 | param 141 | ( 142 | [parameter(Mandatory = $true)] 143 | [System.String] 144 | $Versions, 145 | 146 | [ValidateSet("Present", "Absent")] 147 | [System.String] 148 | $Ensure = 'Present', 149 | 150 | [System.String[]] 151 | $Components = @('All') 152 | ) 153 | 154 | Write-Verbose "Begin executing Test to verify $Versions with $Components are $Ensure" 155 | 156 | [string[]]$splitVersions = $Versions -split ',' | ForEach-Object { $_.Trim() } 157 | 158 | $result = $true 159 | switch ( $Ensure ) { 160 | 'Present' { 161 | foreach ($version in $splitVersions) { 162 | Write-Verbose "Starting test for $version" 163 | $setupComponents = ConvertTo-UnitySetupComponent $Components -Version $version 164 | $setupInstances = Get-UnitySetupInstance | Select-UnitySetupInstance -Version $version 165 | Write-Verbose "Found $($setupInstances.Count) instance(s) of $version" 166 | 167 | if ($setupInstances.Count -eq 0) { 168 | Write-Verbose "Found $version missing." 169 | $result = $false 170 | break 171 | } 172 | 173 | $availableComponents = ($setupInstances[0].Components -band $setupComponents) 174 | if ($availableComponents -ne $setupComponents) { 175 | $missingComponents = ConvertTo-UnitySetupComponent ($setupComponents -bxor $availableComponents) 176 | Write-Verbose "Found $version missing $($missingComponents)" 177 | $result = $false 178 | break 179 | } 180 | } 181 | } 182 | 'Absent' { 183 | foreach ($version in $splitVersions) { 184 | $setupInstances = Get-UnitySetupInstance | Select-UnitySetupInstance -Version $version 185 | Write-Verbose "Found $($setupInstances.Count) instance(s) of $version" 186 | 187 | if ($setupInstances.Count -gt 0) { 188 | Write-Verbose "Found $version installed." 189 | $result = $false 190 | break 191 | } 192 | } 193 | } 194 | } 195 | 196 | $result 197 | 198 | Write-Verbose "End executing Test to verify $Versions with $Components are $Ensure" 199 | } 200 | 201 | 202 | Export-ModuleMember -Function *-TargetResource 203 | 204 | -------------------------------------------------------------------------------- /UnitySetup/DSCResources/xUnitySetupInstance/xUnitySetupInstance.schema.mof: -------------------------------------------------------------------------------- 1 | 2 | [ClassVersion("1.0.0.0"), FriendlyName("xUnitySetupInstance")] 3 | class xUnitySetupInstance : OMI_BaseResource 4 | { 5 | [Key, Description("Comma separated list of Unity versions. ")] string Versions; 6 | [Write, Description("Ensure present or absent?"), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] string Ensure; 7 | [Write, Description("What components to ensure are present?")] string Components[]; 8 | }; 9 | 10 | -------------------------------------------------------------------------------- /UnitySetup/Examples/Sample_xUnity.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | Create a custom configuration by passing in necessary values 3 | #> 4 | Configuration Sample_xUnity { 5 | param 6 | ( 7 | [System.String] 8 | $Version = '2017.4.2f2', 9 | 10 | [ValidateSet('Present', 'Absent')] 11 | [System.String] 12 | $Ensure = 'Present', 13 | 14 | [System.String[]] 15 | $Components = @('Windows', 'Mac', 'Linux', 'UWP', 'iOS', 'Android'), 16 | 17 | [PSCredential] 18 | $UnityCredential, 19 | 20 | [PSCredential] 21 | $UnitySerial 22 | ) 23 | 24 | Import-DscResource -ModuleName UnitySetup 25 | 26 | Node 'localhost' { 27 | 28 | xUnitySetupInstance Unity { 29 | Versions = $Version 30 | Components = $Components 31 | Ensure = $Ensure 32 | DependsOn = if( $Ensure -eq 'Absent' ) { '[xUnityLicense]UnityLicense' } else { $null } 33 | } 34 | 35 | xUnityLicense UnityLicense { 36 | Name = 'UL01' 37 | Credential = $UnityCredential 38 | Serial = $UnitySerial 39 | Ensure = $Ensure 40 | UnityVersion = $Version 41 | DependsOn = if( $Ensure -eq 'Present' ) { '[xUnitySetupInstance]Unity' } else { $null } 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /UnitySetup/Examples/Sample_xUnity_Install.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | Install multiple versions of Unity and several components 3 | #> 4 | Configuration Sample_xUnity_Install { 5 | 6 | param( 7 | [PSCredential]$UnityCredential, 8 | [PSCredential]$UnitySerial 9 | ) 10 | 11 | Import-DscResource -ModuleName UnitySetup 12 | 13 | Node 'localhost' { 14 | 15 | xUnitySetupInstance Unity { 16 | Versions = '2017.4.2f2' 17 | Components = 'Windows', 'Mac', 'Linux', 'UWP', 'iOS', 'Android' 18 | Ensure = 'Present' 19 | } 20 | 21 | xUnityLicense UnityLicense { 22 | Name = 'UL01' 23 | Credential = $UnityCredential 24 | Serial = $UnitySerial 25 | Ensure = 'Present' 26 | UnityVersion = '2017.4.2f2' 27 | DependsOn = '[xUnitySetupInstance]Unity' 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /UnitySetup/Examples/Sample_xUnity_Uninstall.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | Uninstall multiple versions of Unity 3 | #> 4 | Configuration Sample_xUnity_Install { 5 | 6 | param( 7 | [PSCredential]$UnityCredential, 8 | [PSCredential]$UnitySerial 9 | ) 10 | 11 | Import-DscResource -ModuleName UnitySetup 12 | 13 | Node 'localhost' { 14 | 15 | xUnitySetupInstance Unity { 16 | Versions = '2017.4.2f2' 17 | Ensure = 'Absent' 18 | DependsOn = '[xUnityLicense]UnityLicense' 19 | } 20 | 21 | xUnityLicense UnityLicense { 22 | Name = 'UL01' 23 | Credential = $UnityCredential 24 | Serial = $UnitySerial 25 | Ensure = 'Absent' 26 | UnityVersion = '2017.4.2f2' 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /UnitySetup/UnitySetup.psd1: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | # 4 | # Module manifest for module 'PSGet_UnitySetup' 5 | # 6 | # Generated by: Josh Wittner 7 | # 8 | # Generated on: 2018-01-31 9 | # 10 | 11 | @{ 12 | 13 | # Script module or binary module file associated with this manifest. 14 | RootModule = 'UnitySetup' 15 | 16 | # Version number of this module. 17 | ModuleVersion = '6.0' 18 | 19 | # Supported PSEditions 20 | # CompatiblePSEditions = @() 21 | 22 | # ID used to uniquely identify this module 23 | GUID = '6dc524ef-5f56-4bc3-b04d-3c2906898f40' 24 | 25 | # Author of this module 26 | Author = 'Josh Wittner' 27 | 28 | # Company or vendor of this module 29 | CompanyName = 'Microsoft' 30 | 31 | # Copyright statement for this module 32 | Copyright = 'Copyright (c) Microsoft Corporation. All rights reserved.' 33 | 34 | # Description of the functionality provided by this module 35 | Description = 'Tools for managing and automating your Unity installs and projects.' 36 | 37 | # Minimum version of the Windows PowerShell engine required by this module 38 | # PowerShellVersion = '' 39 | 40 | # Name of the Windows PowerShell host required by this module 41 | # PowerShellHostName = '' 42 | 43 | # Minimum version of the Windows PowerShell host required by this module 44 | # PowerShellHostVersion = '' 45 | 46 | # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 47 | # DotNetFrameworkVersion = '' 48 | 49 | # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 50 | # CLRVersion = '' 51 | 52 | # Processor architecture (None, X86, Amd64) required by this module 53 | # ProcessorArchitecture = '' 54 | 55 | # Modules that must be imported into the global environment prior to importing this module 56 | RequiredModules = @( 57 | @{ModuleName = "powershell-yaml"; ModuleVersion = "0.3"; Guid = "6a75a662-7f53-425a-9777-ee61284407da" }, 58 | @{ModuleName = "Az.Accounts"; ModuleVersion = "2.19.0"; Guid = "17a2feff-488b-47f9-8729-e2cec094624c" } 59 | ) 60 | 61 | # Assemblies that must be loaded prior to importing this module 62 | # RequiredAssemblies = @() 63 | 64 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 65 | ScriptsToProcess = @() 66 | 67 | # Type files (.ps1xml) to be loaded when importing this module 68 | # TypesToProcess = @() 69 | 70 | # Format files (.ps1xml) to be loaded when importing this module 71 | # FormatsToProcess = @() 72 | 73 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 74 | # NestedModules = @() 75 | 76 | # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. 77 | 78 | FunctionsToExport = @( 79 | 'Find-UnitySetupInstaller', 80 | 'Select-UnitySetupInstaller', 81 | 'Test-UnitySetupInstance', 82 | 'Get-UnityProjectInstance', 83 | 'Test-UnityProjectInstanceMetaFileIntegrity', 84 | 'Get-UnitySetupInstance', 85 | 'Request-UnitySetupInstaller', 86 | 'Install-UnitySetupInstance', 87 | 'Select-UnitySetupInstance', 88 | 'Uninstall-UnitySetupInstance', 89 | 'Get-UnityEditor', 90 | 'Start-UnityEditor', 91 | 'ConvertTo-UnitySetupComponent', 92 | 'Get-UnityLicense', 93 | 'Import-UnityProjectManifest', 94 | 'Update-UnityPackageManagerConfig' 95 | ) 96 | 97 | # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. 98 | CmdletsToExport = @() 99 | 100 | # Variables to export from this module 101 | VariablesToExport = @() 102 | 103 | # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. 104 | AliasesToExport = @( 105 | 'gusi', 106 | 'gupi', 107 | 'susi', 108 | 'gue', 109 | 'sue' 110 | ) 111 | 112 | # DSC resources to export from this module 113 | # DscResourcesToExport = @() 114 | 115 | # List of all modules packaged with this module 116 | # ModuleList = @() 117 | 118 | # List of all files packaged with this module 119 | # FileList = @() 120 | 121 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 122 | PrivateData = @{ 123 | 124 | PSData = @{ 125 | 126 | # Tags applied to this module. These help with module discovery in online galleries. 127 | Tags = 'Unity' 128 | 129 | # A URL to the license for this module. 130 | LicenseUri = 'https://github.com/Microsoft/unitysetup.powershell/blob/master/LICENSE' 131 | 132 | # A URL to the main website for this project. 133 | ProjectUri = 'https://github.com/Microsoft/unitysetup.powershell' 134 | 135 | # A URL to an icon representing this module. 136 | # IconUri = '' 137 | 138 | # ReleaseNotes of this module 139 | # ReleaseNotes = '' 140 | 141 | # Prerelease string of this module 142 | # Prerelease = '' 143 | 144 | # Flag to indicate whether the module requires explicit user acceptance for install/update 145 | # RequireLicenseAcceptance = $false 146 | 147 | # External dependent modules of this module 148 | # ExternalModuleDependencies = @() 149 | 150 | } # End of PSData hashtable 151 | 152 | } # End of PrivateData hashtable 153 | 154 | # HelpInfo URI of this module 155 | HelpInfoURI = 'https://github.com/Microsoft/unitysetup.powershell/blob/master/README.md' 156 | 157 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 158 | # DefaultCommandPrefix = '' 159 | 160 | } 161 | 162 | -------------------------------------------------------------------------------- /UnitySetup/UnitySetup.psm1: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | Import-Module powershell-yaml -MinimumVersion '0.3' -ErrorAction Stop 4 | 5 | function Get-ModuleVersion { 6 | param ( 7 | [string]$ModuleName 8 | ) 9 | $modules = Get-Module -Name $ModuleName -ListAvailable 10 | if ($modules) { 11 | $highestVersion = $modules | Sort-Object Version -Descending | Select-Object -First 1 12 | return [version]$highestVersion.Version 13 | } 14 | else { 15 | return $null 16 | } 17 | } 18 | 19 | [Flags()] 20 | enum UnitySetupComponent { 21 | Windows = (1 -shl 0) 22 | Linux = (1 -shl 1) 23 | Mac = (1 -shl 2) 24 | Documentation = (1 -shl 3) 25 | StandardAssets = (1 -shl 4) 26 | Windows_IL2CPP = (1 -shl 5) 27 | Metro = (1 -shl 6) 28 | UWP = (1 -shl 6) 29 | UWP_IL2CPP = (1 -shl 7) 30 | Android = (1 -shl 8) 31 | iOS = (1 -shl 9) 32 | AppleTV = (1 -shl 10) 33 | Facebook = (1 -shl 11) 34 | Vuforia = (1 -shl 12) 35 | WebGL = (1 -shl 13) 36 | Mac_IL2CPP = (1 -shl 14) 37 | Lumin = (1 -shl 15) 38 | Linux_IL2CPP = (1 -shl 16) 39 | Windows_Server = (1 -shl 17) 40 | VisionOS = (1 -shl 18) 41 | All = (1 -shl 19) - 1 42 | } 43 | 44 | [Flags()] 45 | enum OperatingSystem { 46 | Windows 47 | Linux 48 | Mac 49 | } 50 | 51 | class UnitySetupResource { 52 | [UnitySetupComponent] $ComponentType 53 | [string] $Path 54 | } 55 | 56 | class UnitySetupInstaller { 57 | [UnitySetupComponent] $ComponentType 58 | [UnityVersion] $Version 59 | [int64]$Length 60 | [DateTime]$LastModified 61 | [string]$DownloadUrl 62 | } 63 | 64 | class UnitySetupInstance { 65 | [UnityVersion]$Version 66 | [UnitySetupComponent]$Components 67 | [string]$Path 68 | 69 | UnitySetupInstance([string]$path) { 70 | $currentOS = Get-OperatingSystem 71 | 72 | $this.Path = $path 73 | $this.Version = Get-UnitySetupInstanceVersion -Path $path 74 | if ( -not $this.Version ) { throw "Unable to find version for $path" } 75 | 76 | $playbackEnginePath = $null 77 | $componentTests = switch ($currentOS) { 78 | ([OperatingSystem]::Windows) { 79 | $this.Components = [UnitySetupComponent]::Windows 80 | $playbackEnginePath = [io.path]::Combine("$Path", "Editor\Data\PlaybackEngines"); 81 | @{ 82 | [UnitySetupComponent]::Documentation = , [io.path]::Combine("$Path", "Editor\Data\Documentation"); 83 | [UnitySetupComponent]::StandardAssets = , [io.path]::Combine("$Path", "Editor\Standard Assets"); 84 | [UnitySetupComponent]::Windows_IL2CPP = , [io.path]::Combine("$playbackEnginePath", "windowsstandalonesupport\Variations\win32_development_il2cpp"), 85 | [io.path]::Combine("$playbackEnginePath", "windowsstandalonesupport\Variations\win32_player_development_il2cpp"); 86 | [UnitySetupComponent]::UWP = [io.path]::Combine("$playbackEnginePath", "MetroSupport\Templates\UWP_.NET_D3D"), 87 | [io.path]::Combine("$playbackEnginePath", "MetroSupport\Templates\UWP_D3D"); 88 | [UnitySetupComponent]::UWP_IL2CPP = , [io.path]::Combine("$playbackEnginePath", "MetroSupport\Templates\UWP_IL2CPP_D3D"); 89 | [UnitySetupComponent]::Linux = , [io.path]::Combine("$playbackEnginePath", "LinuxStandaloneSupport\Variations\linux64_headless_development_mono"); 90 | [UnitySetupComponent]::Linux_IL2CPP = , [io.path]::Combine("$playbackEnginePath", "LinuxStandaloneSupport\Variations\linux64_headless_development_il2cpp"); 91 | [UnitySetupComponent]::Mac = , [io.path]::Combine("$playbackEnginePath", "MacStandaloneSupport"); 92 | [UnitySetupComponent]::Windows_Server = , [io.path]::Combine("$playbackEnginePath", "WindowsStandaloneSupport\Variations\win32_player_development_mono"), 93 | [io.path]::Combine("$playbackEnginePath", "WindowsStandaloneSupport\Variations\win32_server_development_il2cpp"), 94 | [io.path]::Combine("$playbackEnginePath", "WindowsStandaloneSupport\Variations\win32_server_development_mono"), 95 | [io.path]::Combine("$playbackEnginePath", "WindowsStandaloneSupport\Variations\win64_player_development_mono"), 96 | [io.path]::Combine("$playbackEnginePath", "WindowsStandaloneSupport\Variations\win64_server_development_il2cpp"), 97 | [io.path]::Combine("$playbackEnginePath", "WindowsStandaloneSupport\Variations\win64_server_development_mono"); 98 | } 99 | } 100 | ([OperatingSystem]::Linux) { 101 | $this.Components = [UnitySetupComponent]::Linux 102 | 103 | throw "UnitySetupInstance has not been implemented on the Linux platform. Contributions welcomed!"; 104 | } 105 | ([OperatingSystem]::Mac) { 106 | $this.Components = [UnitySetupComponent]::Mac 107 | $playbackEnginePath = [io.path]::Combine("$Path", "PlaybackEngines"); 108 | @{ 109 | [UnitySetupComponent]::Documentation = , [io.path]::Combine("$Path", "Documentation"); 110 | [UnitySetupComponent]::StandardAssets = , [io.path]::Combine("$Path", "Standard Assets"); 111 | [UnitySetupComponent]::Mac_IL2CPP = , [io.path]::Combine("$playbackEnginePath", "MacStandaloneSupport/Variations/macosx64_development_il2cpp"); 112 | [UnitySetupComponent]::Windows = , [io.path]::Combine("$playbackEnginePath", "WindowsStandaloneSupport"); 113 | [UnitySetupComponent]::Linux = , [io.path]::Combine("$playbackEnginePath", "LinuxStandaloneSupport/Variations/linux64_headless_development_mono"); 114 | [UnitySetupComponent]::Linux_IL2CPP = , [io.path]::Combine("$playbackEnginePath", "LinuxStandaloneSupport/Variations/linux64_headless_development_il2cpp"); 115 | } 116 | } 117 | } 118 | 119 | # Common playback engines: 120 | $componentTests[[UnitySetupComponent]::Lumin] = , [io.path]::Combine("$playbackEnginePath", "LuminSupport"); 121 | $componentTests[[UnitySetupComponent]::Android] = , [io.path]::Combine("$playbackEnginePath", "AndroidPlayer"); 122 | $componentTests[[UnitySetupComponent]::iOS] = , [io.path]::Combine("$playbackEnginePath", "iOSSupport"); 123 | $componentTests[[UnitySetupComponent]::AppleTV] = , [io.path]::Combine("$playbackEnginePath", "AppleTVSupport"); 124 | $componentTests[[UnitySetupComponent]::Facebook] = , [io.path]::Combine("$playbackEnginePath", "Facebook"); 125 | $componentTests[[UnitySetupComponent]::Vuforia] = , [io.path]::Combine("$playbackEnginePath", "VuforiaSupport"); 126 | $componentTests[[UnitySetupComponent]::WebGL] = , [io.path]::Combine("$playbackEnginePath", "WebGLSupport"); 127 | $componentTests[[UnitySetupComponent]::VisionOS] = , [io.path]::Combine("$playbackEnginePath", "VisionOSPlayer"); 128 | 129 | $componentTests.Keys | ForEach-Object { 130 | foreach ( $test in $componentTests[$_] ) { 131 | if ( Test-Path -PathType Container -Path $test ) { 132 | $this.Components += $_ 133 | break; 134 | } 135 | } 136 | } 137 | } 138 | } 139 | 140 | class UnityProjectInstance { 141 | [UnityVersion]$Version 142 | [string]$Path 143 | [string]$ProductName 144 | 145 | UnityProjectInstance([string]$path) { 146 | $versionFile = [io.path]::Combine($path, "ProjectSettings\ProjectVersion.txt") 147 | if (!(Test-Path $versionFile)) { throw "Path is not a Unity project: $path" } 148 | 149 | $fileVersion = (Get-Content $versionFile -Raw | ConvertFrom-Yaml)['m_EditorVersion']; 150 | if (!$fileVersion) { throw "Project is missing a version in: $versionFile" } 151 | 152 | $projectSettingsFile = [io.path]::Combine($path, "ProjectSettings\ProjectSettings.asset") 153 | if (!(Test-Path $projectSettingsFile)) { throw "Project is missing ProjectSettings.asset" } 154 | 155 | try { 156 | $prodName = ((Get-Content $projectSettingsFile -Raw | ConvertFrom-Yaml)['playerSettings'])['productName'] 157 | if (!$prodName) { throw "ProjectSettings is missing productName" } 158 | } 159 | catch { 160 | $msg = "Could not read $projectSettingsFile, in the Unity project try setting Editor Settings > Asset Serialiazation Mode to 'Force Text'." 161 | $msg += "`nAn Exception was caught!" 162 | $msg += "`nException Type: $($_.Exception.GetType().FullName)" 163 | $msg += "`nException Message: $($_.Exception.Message)" 164 | Write-Warning -Message $msg 165 | 166 | $prodName = $null 167 | } 168 | 169 | $this.Path = $path 170 | $this.Version = $fileVersion 171 | $this.ProductName = $prodName 172 | } 173 | } 174 | 175 | class UnityVersion : System.IComparable { 176 | [int] $Major; 177 | [int] $Minor; 178 | [int] $Revision; 179 | [char] $Release; 180 | [int] $Build; 181 | [string] $Suffix; 182 | 183 | [string] ToString() { 184 | $result = "$($this.Major).$($this.Minor).$($this.Revision)$($this.Release)$($this.Build)" 185 | if ( $this.Suffix ) { $result += "-$($this.Suffix)" } 186 | return $result 187 | } 188 | 189 | UnityVersion([string] $version) { 190 | $parts = $version.Split('-') 191 | 192 | $parts[0] -match "(\d+)\.(\d+)\.(\d+)([fpba])(\d+)" | Out-Null 193 | if ( $Matches.Count -ne 6 ) { throw "Invalid unity version: $version" } 194 | $this.Major = [int]($Matches[1]); 195 | $this.Minor = [int]($Matches[2]); 196 | $this.Revision = [int]($Matches[3]); 197 | $this.Release = [char]($Matches[4]); 198 | $this.Build = [int]($Matches[5]); 199 | 200 | if ($parts.Length -gt 1) { 201 | $this.Suffix = $parts[1]; 202 | } 203 | } 204 | 205 | [int] CompareTo([object]$obj) { 206 | if ($null -eq $obj) { return 1 } 207 | if ($obj -isnot [UnityVersion]) { throw "Object is not a UnityVersion" } 208 | 209 | return [UnityVersion]::Compare($this, $obj) 210 | } 211 | 212 | static [int] Compare([UnityVersion]$a, [UnityVersion]$b) { 213 | if ($a.Major -lt $b.Major) { return -1 } 214 | if ($a.Major -gt $b.Major) { return 1 } 215 | 216 | if ($a.Minor -lt $b.Minor) { return -1 } 217 | if ($a.Minor -gt $b.Minor) { return 1 } 218 | 219 | if ($a.Revision -lt $b.Revision) { return -1 } 220 | if ($a.Revision -gt $b.Revision) { return 1 } 221 | 222 | if ($a.Release -lt $b.Release) { return -1 } 223 | if ($a.Release -gt $b.Release) { return 1 } 224 | 225 | if ($a.Build -lt $b.Build) { return -1 } 226 | if ($a.Build -gt $b.Build) { return 1 } 227 | 228 | if ($a.Suffix -lt $b.Suffix) { return -1 } 229 | if ($a.Suffix -gt $b.Suffix) { return 1 } 230 | 231 | return 0 232 | } 233 | } 234 | 235 | <# 236 | .Synopsis 237 | Easy way to determine the current operating system platform being executed on. 238 | .DESCRIPTION 239 | Determine which operating system that's executing the script for things like path variants. 240 | .OUTPUTS 241 | Get-OperatingSystem returns a [OperatingSystem] enumeration based off the Powershell platform being run on. 242 | .EXAMPLE 243 | $OS = Get-OperatingSystem 244 | .EXAMPLE 245 | # Loosely typed. 246 | switch (Get-OperatingSystem) { 247 | Windows { echo "On Windows" } 248 | Linux { echo "On Linux" } 249 | Mac { echo "On Mac" } 250 | } 251 | .EXAMPLE 252 | # Strongly typed. 253 | switch (Get-OperatingSystem) { 254 | ([OperatingSystem]::Windows) { echo "On Windows" } 255 | ([OperatingSystem]::Linux) { echo "On Linux" } 256 | ([OperatingSystem]::Mac) { echo "On Mac" } 257 | } 258 | .EXAMPLE 259 | if (Get-OperatingSystem -eq [OperatingSystem]::Linux) { 260 | echo "On Linux" 261 | } 262 | #> 263 | function Get-OperatingSystem { 264 | if ((-not $global:PSVersionTable.Platform) -or ($global:PSVersionTable.Platform -eq "Win32NT")) { 265 | return [OperatingSystem]::Windows 266 | } 267 | elseif ($global:PSVersionTable.OS.Contains("Linux")) { 268 | return [OperatingSystem]::Linux 269 | } 270 | elseif ($global:PSVersionTable.OS.Contains("Darwin")) { 271 | return [OperatingSystem]::Mac 272 | } 273 | } 274 | 275 | <# 276 | .Synopsis 277 | Get the Unity Editor application 278 | .PARAMETER Path 279 | Path of a UnitySetupInstance 280 | .EXAMPLE 281 | Get-UnityEditor -Path $unitySetupInstance.Path 282 | #> 283 | function Get-UnityEditor { 284 | [CmdletBinding()] 285 | param( 286 | [ValidateScript( { Test-Path $_ -PathType Container } )] 287 | [Parameter(Mandatory = $false, ValueFromPipeline = $true, Position = 0, ParameterSetName = "Path")] 288 | [string[]]$Path = $PWD, 289 | 290 | [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0, ParameterSetName = "Instance")] 291 | [ValidateNotNull()] 292 | [UnitySetupInstance[]]$Instance 293 | ) 294 | 295 | process { 296 | 297 | if ( $PSCmdlet.ParameterSetName -eq "Instance" ) { 298 | $Path = $Instance.Path 299 | } 300 | 301 | $currentOS = Get-OperatingSystem 302 | foreach ($p in $Path) { 303 | switch ($currentOS) { 304 | ([OperatingSystem]::Windows) { 305 | $editor = Join-Path "$p" 'Editor\Unity.exe' 306 | 307 | if (Test-Path $editor) { 308 | Write-Output (Resolve-Path $editor).Path 309 | } 310 | } 311 | ([OperatingSystem]::Linux) { 312 | throw "Get-UnityEditor has not been implemented on the Linux platform. Contributions welcomed!"; 313 | } 314 | ([OperatingSystem]::Mac) { 315 | $editor = Join-Path "$p" "Unity.app/Contents/MacOS/Unity" 316 | 317 | if (Test-Path $editor) { 318 | Write-Output (Resolve-Path $editor).Path 319 | } 320 | } 321 | } 322 | } 323 | } 324 | } 325 | 326 | <# 327 | .Synopsis 328 | Help to create UnitySetupComponent 329 | .PARAMETER Components 330 | What components would you like included? 331 | .PARAMETER Version 332 | Allows for conversion that can take into account version restrictions 333 | E.g. 2019.x only supports UWP_IL2CPP 334 | .EXAMPLE 335 | ConvertTo-UnitySetupComponent Windows,UWP 336 | .EXAMPLE 337 | ConvertTo-UnitySetupComponent Windows,UWP -Version 2019.3.4f1 338 | #> 339 | function ConvertTo-UnitySetupComponent { 340 | [CmdletBinding()] 341 | param( 342 | [parameter(Mandatory = $true, Position = 0)] 343 | [UnitySetupComponent] $Component, 344 | [parameter(Mandatory = $false)] 345 | [UnityVersion] $Version 346 | ) 347 | 348 | $currentOS = Get-OperatingSystem 349 | 350 | if ($Version) { 351 | if ($Version.Major -ge 2019) { 352 | if ($Component -band [UnitySetupComponent]::UWP) { 353 | if ( $Component -band [UnitySetupComponent]::UWP_IL2CPP) { 354 | Write-Verbose "2019.x only supports IL2CPP for UWP - removing $([UnitySetupComponent]::UWP)" 355 | } 356 | else { 357 | Write-Verbose "2019.x only supports IL2CPP for UWP - swapping to $([UnitySetupComponent]::UWP_IL2CPP)" 358 | $Component += [UnitySetupComponent]::UWP_IL2CPP; 359 | } 360 | 361 | $Component -= [UnitySetupComponent]::UWP; 362 | } 363 | } 364 | 365 | if ($Component -band [UnitySetupComponent]::VisionOS -and ( ` 366 | $currentOS -notin [OperatingSystem]::Windows, [OperatingSystem]::Mac ` 367 | -or $currentOS -eq [OperatingSystem]::Mac -and [UnityVersion]::Compare($Version, [UnityVersion]"2022.3.18f1") -lt 0 ` 368 | -or $currentOS -eq [OperatingSystem]::Windows -and [UnityVersion]::Compare($Version, [UnityVersion]"2022.3.21f1") -lt 0 ` 369 | )) { 370 | Write-Verbose "VisionOS is only supported starting in Unity version 2022.3.18f1 on Mac or 2022.3.21f1 on Windows - removing $([UnitySetupComponent]::VisionOS)" 371 | $Component -= [UnitySetupComponent]::VisionOS; 372 | } 373 | } 374 | 375 | $Component 376 | } 377 | 378 | <# 379 | .Synopsis 380 | Finds UnitySetup installers for a specified version. 381 | .DESCRIPTION 382 | Finds UnitySetup component installers for a specified version by querying Unity's website. 383 | .PARAMETER Version 384 | What version of Unity are you looking for? 385 | .PARAMETER Hash 386 | Manually specify the build hash, to select a private build. 387 | .PARAMETER Components 388 | What components would you like to search for? Defaults to All 389 | .EXAMPLE 390 | Find-UnitySetupInstaller -Version 2017.3.0f3 391 | .EXAMPLE 392 | Find-UnitySetupInstaller -Version 2017.3.0f3 -Components Windows,Documentation 393 | #> 394 | function Find-UnitySetupInstaller { 395 | [CmdletBinding()] 396 | param( 397 | [parameter(Mandatory = $true)] 398 | [UnityVersion] $Version, 399 | 400 | [parameter(Mandatory = $false)] 401 | [UnitySetupComponent] $Components = [UnitySetupComponent]::All, 402 | 403 | [parameter(Mandatory = $false)] 404 | [string] $Hash = "", 405 | 406 | [parameter(Mandatory = $false)] 407 | [OperatingSystem] $ExplicitOS 408 | ) 409 | 410 | $Components = ConvertTo-UnitySetupComponent -Component $Components -Version $Version 411 | 412 | if ($ExplicitOS) { 413 | $currentOS = $ExplicitOS 414 | } 415 | else { 416 | $currentOS = Get-OperatingSystem 417 | } 418 | 419 | switch ($currentOS) { 420 | ([OperatingSystem]::Windows) { 421 | $targetSupport = "TargetSupportInstaller" 422 | $installerExtension = "exe" 423 | } 424 | ([OperatingSystem]::Linux) { 425 | throw "Find-UnitySetupInstaller has not been implemented on the Linux platform. Contributions welcomed!"; 426 | } 427 | ([OperatingSystem]::Mac) { 428 | $targetSupport = "MacEditorTargetInstaller" 429 | $installerExtension = "pkg" 430 | } 431 | } 432 | 433 | $unitySetupRegEx = "^(.+)\/([a-z0-9]+)\/(.+)\/(.+)-(\d+)\.(\d+)\.(\d+)([fpba])(\d+).$installerExtension$" 434 | 435 | $knownBaseUrls = @( 436 | "https://download.unity3d.com/download_unity", 437 | "https://netstorage.unity3d.com/unity", 438 | "https://beta.unity3d.com/download" 439 | ) 440 | 441 | $installerTemplates = @{ 442 | [UnitySetupComponent]::UWP = "$targetSupport/UnitySetup-UWP-.NET-Support-for-Editor-$Version.$installerExtension", 443 | "$targetSupport/UnitySetup-Metro-Support-for-Editor-$Version.$installerExtension", 444 | "$targetSupport/UnitySetup-Universal-Windows-Platform-Support-for-Editor-$Version.$installerExtension"; 445 | [UnitySetupComponent]::UWP_IL2CPP = , "$targetSupport/UnitySetup-UWP-IL2CPP-Support-for-Editor-$Version.$installerExtension"; 446 | [UnitySetupComponent]::Android = , "$targetSupport/UnitySetup-Android-Support-for-Editor-$Version.$installerExtension"; 447 | [UnitySetupComponent]::iOS = , "$targetSupport/UnitySetup-iOS-Support-for-Editor-$Version.$installerExtension"; 448 | [UnitySetupComponent]::AppleTV = , "$targetSupport/UnitySetup-AppleTV-Support-for-Editor-$Version.$installerExtension"; 449 | [UnitySetupComponent]::Facebook = , "$targetSupport/UnitySetup-Facebook-Games-Support-for-Editor-$Version.$installerExtension"; 450 | [UnitySetupComponent]::Linux = "$targetSupport/UnitySetup-Linux-Support-for-Editor-$Version.$installerExtension", 451 | "$targetSupport/UnitySetup-Linux-Mono-Support-for-Editor-$Version.$installerExtension"; 452 | [UnitySetupComponent]::Mac = "$targetSupport/UnitySetup-Mac-Support-for-Editor-$Version.$installerExtension", 453 | "$targetSupport/UnitySetup-Mac-Mono-Support-for-Editor-$Version.$installerExtension"; 454 | [UnitySetupComponent]::Mac_IL2CPP = , "$targetSupport/UnitySetup-Mac-IL2CPP-Support-for-Editor-$Version.$installerExtension"; 455 | [UnitySetupComponent]::Vuforia = , "$targetSupport/UnitySetup-Vuforia-AR-Support-for-Editor-$Version.$installerExtension"; 456 | [UnitySetupComponent]::WebGL = , "$targetSupport/UnitySetup-WebGL-Support-for-Editor-$Version.$installerExtension"; 457 | [UnitySetupComponent]::Windows_IL2CPP = , "$targetSupport/UnitySetup-Windows-IL2CPP-Support-for-Editor-$Version.$installerExtension"; 458 | [UnitySetupComponent]::Lumin = , "$targetSupport/UnitySetup-Lumin-Support-for-Editor-$Version.$installerExtension"; 459 | [UnitySetupComponent]::Linux_IL2CPP = , "$targetSupport/UnitySetup-Linux-IL2CPP-Support-for-Editor-$Version.$installerExtension"; 460 | [UnitySetupComponent]::Windows_Server = , "$targetSupport/UnitySetup-Windows-Server-Support-for-Editor-$Version.$installerExtension"; 461 | [UnitySetupComponent]::VisionOS = , "$targetSupport/UnitySetup-VisionOS-Support-for-Editor-$Version.$installerExtension"; 462 | } 463 | 464 | # In 2019.x there is only IL2CPP UWP so change the search for UWP_IL2CPP 465 | if ( $Version.Major -ge 2019 ) { 466 | $installerTemplates[[UnitySetupComponent]::UWP_IL2CPP] = @( 467 | "$targetSupport/UnitySetup-Universal-Windows-Platform-Support-for-Editor-$Version.$installerExtension"); 468 | } 469 | 470 | switch ($currentOS) { 471 | ([OperatingSystem]::Windows) { 472 | $setupComponent = [UnitySetupComponent]::Windows 473 | $installerTemplates[$setupComponent] = , "Windows64EditorInstaller/UnitySetup64-$Version.exe"; 474 | 475 | $installerTemplates[[UnitySetupComponent]::Documentation] = , "WindowsDocumentationInstaller/UnityDocumentationSetup-$Version.exe"; 476 | $installerTemplates[[UnitySetupComponent]::StandardAssets] = , "WindowsStandardAssetsInstaller/UnityStandardAssetsSetup-$Version.exe"; 477 | } 478 | ([OperatingSystem]::Linux) { 479 | $setupComponent = [UnitySetupComponent]::Linux 480 | # TODO: $installerTemplates[$setupComponent] = , "???/UnitySetup64-$Version.exe"; 481 | 482 | throw "Find-UnitySetupInstaller has not been implemented on the Linux platform. Contributions welcomed!"; 483 | } 484 | ([OperatingSystem]::Mac) { 485 | $setupComponent = [UnitySetupComponent]::Mac 486 | $installerTemplates[$setupComponent] = , "MacEditorInstaller/Unity-$Version.pkg"; 487 | 488 | # Note: These links appear to be unavailable even on Unity's website for 2018. 489 | # StandardAssets appears to work if you select a 2017 version. 490 | $installerTemplates[[UnitySetupComponent]::Documentation] = , "MacDocumentationInstaller/DocumentationSetup-$Version.pkg"; 491 | $installerTemplates[[UnitySetupComponent]::StandardAssets] = , "MacStandardAssetsInstaller/StandardAssets-$Version.pkg"; 492 | } 493 | } 494 | 495 | # By default Tls12 protocol is not enabled, but is what backs Unity's website, so enable it 496 | $secProtocol = [System.Net.ServicePointManager]::SecurityProtocol 497 | if ( ($secProtocol -band [System.Net.SecurityProtocolType]::Tls12) -eq 0 ) { 498 | $secProtocol += [System.Net.SecurityProtocolType]::Tls12; 499 | [System.Net.ServicePointManager]::SecurityProtocol = $secProtocol 500 | } 501 | 502 | # Every release type has a different pattern for finding installers 503 | $searchPages = @() 504 | switch ($Version.Release) { 505 | 'a' { $searchPages += "https://unity3d.com/alpha/$($Version.Major).$($Version.Minor)" } 506 | 'b' { 507 | $searchPages += "https://unity3d.com/unity/beta/unity$Version", 508 | "https://unity3d.com/unity/beta/$($Version.Major).$($Version.Minor)", 509 | "https://unity3d.com/unity/beta/$Version" 510 | } 511 | 'f' { 512 | $searchPages += "https://unity3d.com/get-unity/download/archive", 513 | "https://unity3d.com/unity/whats-new/$($Version.Major).$($Version.Minor).$($Version.Revision)" 514 | 515 | # Just in case it's a release candidate search the beta as well. 516 | if ($Version.Revision -eq '0') { 517 | $searchPages += "https://unity3d.com/unity/beta/unity$Version", 518 | "https://unity3d.com/unity/beta/$($Version.Major).$($Version.Minor)", 519 | "https://unity3d.com/unity/beta/$Version" 520 | } 521 | } 522 | 'p' { 523 | $patchPage = "https://unity3d.com/unity/qa/patch-releases?version=$($Version.Major).$($Version.Minor)" 524 | $searchPages += $patchPage 525 | 526 | $webResult = Invoke-WebRequest $patchPage -UseBasicParsing 527 | $searchPages += $webResult.Links | 528 | Where-Object { $_.href -match "\/unity\/qa\/patch-releases\?version=$($Version.Major)\.$($Version.Minor)&page=(\d+)" -and $Matches[1] -gt 1 } | 529 | ForEach-Object { "https://unity3d.com$($_.href)" } 530 | } 531 | } 532 | 533 | if($Hash -ne ""){ 534 | $searchPages += "http://beta.unity3d.com/download/$Hash/download.html" 535 | } 536 | 537 | foreach ($page in $searchPages) { 538 | try { 539 | Write-Verbose "Searching page - $page" 540 | $webResult = Invoke-WebRequest $page -UseBasicParsing 541 | $prototypeLink = $webResult.Links | 542 | Select-Object -ExpandProperty href -ErrorAction SilentlyContinue | 543 | Where-Object { 544 | $link = $_ 545 | 546 | foreach ( $installer in $installerTemplates.Keys ) { 547 | foreach ( $template in $installerTemplates[$installer] ) { 548 | if ( $link -like "*$template*" ) { return $true } 549 | } 550 | } 551 | 552 | return $false 553 | } | 554 | Select-Object -First 1 555 | 556 | if ($null -ne $prototypeLink) 557 | { 558 | # Ensure prototype link is absolute uri 559 | if(-not [system.uri]::IsWellFormedUriString($_,[System.UriKind]::Absolute)) { 560 | $prototypeLink = "$([system.uri]::new([system.uri]$page, [system.uri]$prototypeLink))" 561 | } 562 | 563 | break 564 | } 565 | } 566 | catch { 567 | Write-Verbose "$page failed: $($_.Exception.Message)" 568 | } 569 | } 570 | 571 | if ($null -eq $prototypeLink) { 572 | throw "Could not find archives for Unity version $Version" 573 | } 574 | 575 | Write-Verbose "Prototype link found: $prototypeLink" 576 | $linkComponents = $prototypeLink -split $unitySetupRegEx -ne "" 577 | 578 | if ($knownBaseUrls -notcontains $linkComponents[0]) { 579 | $knownBaseUrls = $linkComponents[0], $knownBaseUrls 580 | } 581 | else { 582 | $knownBaseUrls = $knownBaseUrls | Sort-Object -Property @{ Expression = { [math]::Abs(($_.CompareTo($linkComponents[0]))) }; Ascending = $true } 583 | } 584 | 585 | if ($Hash -ne "") { 586 | $linkComponents[1] = $Hash 587 | } 588 | 589 | $installerTemplates.Keys | Where-Object { $Components -band $_ } | ForEach-Object { 590 | $templates = $installerTemplates.Item($_); 591 | $result = $null 592 | foreach ($template in $templates ) { 593 | foreach ( $baseUrl in $knownBaseUrls) { 594 | $endpoint = [uri][System.IO.Path]::Combine($baseUrl, $linkComponents[1], $template); 595 | try { 596 | Write-Verbose "Attempting to get component $_ details from endpoint: $endpoint" 597 | $testResult = Invoke-WebRequest $endpoint -Method HEAD -UseBasicParsing 598 | # For packages on macOS the Content-Length and Last-Modified are returned as an array. 599 | if ($testResult.Headers['Content-Length'] -is [System.Array]) { 600 | $installerLength = [int64]$testResult.Headers['Content-Length'][0] 601 | } 602 | else { 603 | $installerLength = [int64]$testResult.Headers['Content-Length'] 604 | } 605 | if ($testResult.Headers['Last-Modified'] -is [System.Array]) { 606 | $lastModified = [System.DateTime]$testResult.Headers['Last-Modified'][0] 607 | } 608 | else { 609 | $lastModified = [System.DateTime]$testResult.Headers['Last-Modified'] 610 | } 611 | $result = New-Object UnitySetupInstaller -Property @{ 612 | 'ComponentType' = $_; 613 | 'Version' = $Version; 614 | 'DownloadUrl' = $endpoint; 615 | 'Length' = $installerLength; 616 | 'LastModified' = $lastModified; 617 | } 618 | 619 | break 620 | } 621 | catch { 622 | Write-Verbose "$endpoint failed: $($_.Exception.Message)" 623 | } 624 | } 625 | 626 | if ( $result ) { break } 627 | } 628 | 629 | if ( -not $result ) { 630 | Write-Warning "Unable to find installer for the $_ component." 631 | } 632 | else { $result } 633 | } | Sort-Object -Property ComponentType 634 | } 635 | 636 | <# 637 | .Synopsis 638 | Test if a Unity instance is installed. 639 | .DESCRIPTION 640 | Returns the status of a Unity install by Version and/or Path to install. 641 | .PARAMETER Version 642 | What version of Unity are you looking for? 643 | .PARAMETER BasePath 644 | Under what base patterns is Unity customly installed at. 645 | .PARAMETER Path 646 | Exact path you expect Unity to be installed at. 647 | .EXAMPLE 648 | Test-UnitySetupInstance -Version 2017.3.0f3 649 | .EXAMPLE 650 | Test-UnitySetupInstance -BasePath D:/UnityInstalls/Unity2018 651 | #> 652 | function Test-UnitySetupInstance { 653 | [CmdletBinding()] 654 | param( 655 | [parameter(Mandatory = $false)] 656 | [UnityVersion] $Version, 657 | 658 | [parameter(Mandatory = $false)] 659 | [string] $BasePath, 660 | 661 | [parameter(Mandatory = $false)] 662 | [string] $Path 663 | ) 664 | 665 | $instance = Get-UnitySetupInstance -BasePath $BasePath | Select-UnitySetupInstance -Version $Version -Path $Path 666 | return $null -ne $instance 667 | } 668 | 669 | <# 670 | .Synopsis 671 | Select installers by a version and/or components. 672 | .DESCRIPTION 673 | Filters a list of `UnitySetupInstaller` down to a specific version and/or specific components. 674 | .PARAMETER Installers 675 | List of installers that needs to be reduced. 676 | .PARAMETER Version 677 | What version of UnitySetupInstaller that you want to keep. 678 | .PARAMETER Components 679 | What components should be maintained. 680 | .EXAMPLE 681 | $installers = Find-UnitySetupInstaller -Version 2017.3.0f3 682 | $installers += Find-UnitySetupInstaller -Version 2018.2.5f1 683 | $installers | Select-UnitySetupInstaller -Component Windows,Linux,Mac 684 | #> 685 | function Select-UnitySetupInstaller { 686 | [CmdletBinding()] 687 | param( 688 | [parameter(ValueFromPipeline = $true)] 689 | [UnitySetupInstaller[]] $Installers, 690 | 691 | [parameter(Mandatory = $false)] 692 | [UnityVersion] $Version, 693 | 694 | [parameter(Mandatory = $false)] 695 | [UnitySetupComponent] $Components = [UnitySetupComponent]::All 696 | ) 697 | begin { 698 | $selectedInstallers = @() 699 | } 700 | process { 701 | # Keep only the matching version specified. 702 | if ( $PSBoundParameters.ContainsKey('Version') ) { 703 | $Installers = $Installers | Where-Object { [UnityVersion]::Compare($_.Version, $Version) -eq 0 } 704 | } 705 | 706 | # Keep only the matching component(s). 707 | foreach ($installer in $Installers) { 708 | $versionComponents = ConvertTo-UnitySetupComponent $Components -Version $installer.Version 709 | if ( $versionComponents -band $_.ComponentType ) { 710 | $selectedInstallers += $installer 711 | } 712 | } 713 | } 714 | end { 715 | return $selectedInstallers 716 | } 717 | } 718 | 719 | filter Format-Bytes { 720 | return "{0:N2} {1}" -f $( 721 | if ($_ -lt 1kb) { $_, 'Bytes' } 722 | elseif ($_ -lt 1mb) { ($_ / 1kb), 'KB' } 723 | elseif ($_ -lt 1gb) { ($_ / 1mb), 'MB' } 724 | elseif ($_ -lt 1tb) { ($_ / 1gb), 'GB' } 725 | elseif ($_ -lt 1pb) { ($_ / 1tb), 'TB' } 726 | else { ($_ / 1pb), 'PB' } 727 | ) 728 | } 729 | 730 | function Format-BitsPerSecond { 731 | [CmdletBinding()] 732 | [OutputType([string])] 733 | param( 734 | [parameter(Mandatory = $true)] 735 | [int64] $Bytes, 736 | 737 | [parameter(Mandatory = $true)] 738 | [int] $Seconds 739 | ) 740 | if ($Seconds -le 0.001) { 741 | return "0 Bps" 742 | } 743 | # Convert from bytes to bits 744 | $Bits = ($Bytes * 8) / $Seconds 745 | return "{0:N2} {1}" -f $( 746 | if ($Bits -lt 1kb) { $Bits, 'Bps' } 747 | elseif ($Bits -lt 1mb) { ($Bits / 1kb), 'Kbps' } 748 | elseif ($Bits -lt 1gb) { ($Bits / 1mb), 'Mbps' } 749 | elseif ($Bits -lt 1tb) { ($Bits / 1gb), 'Gbps' } 750 | elseif ($Bits -lt 1pb) { ($Bits / 1tb), 'Tbps' } 751 | else { ($Bits / 1pb), 'Pbps' } 752 | ) 753 | } 754 | 755 | <# 756 | .Synopsis 757 | Download specified Unity installers. 758 | .DESCRIPTION 759 | Downloads the given installers into the $Cache directory. 760 | .PARAMETER Installers 761 | List of installers that needs to be downloaded. 762 | .PARAMETER Cache 763 | File path where installers will be downloaded to. 764 | .EXAMPLE 765 | $installers = Find-UnitySetupInstaller -Version 2017.3.0f3 766 | Request-UnitySetupInstaller -Installers $installers 767 | .EXAMPLE 768 | Find-UnitySetupInstaller -Version 2017.3.0f3 | Request-UnitySetupInstaller 769 | #> 770 | function Request-UnitySetupInstaller { 771 | [CmdletBinding()] 772 | [OutputType([Object[]])] 773 | param( 774 | [parameter(ValueFromPipeline = $true)] 775 | [UnitySetupInstaller[]] $Installers, 776 | 777 | [parameter(Mandatory = $false)] 778 | [string]$Cache = [io.Path]::Combine("~", ".unitysetup") 779 | ) 780 | begin { 781 | # Note that this has to happen before calculating the full path since 782 | # Resolve-Path throws an exception on missing paths. 783 | if (!(Test-Path $Cache -PathType Container)) { 784 | New-Item $Cache -ItemType Directory -ErrorAction Stop | Out-Null 785 | } 786 | 787 | # Expanding '~' to the absolute path on the system. `WebClient` on macOS asumes 788 | # relative path. macOS also treats alt directory separators as part of the file 789 | # name and this corrects the separators to current environment. 790 | $fullCachePath = (Resolve-Path -Path $Cache).Path 791 | 792 | $allInstallers = @() 793 | } 794 | process { 795 | # Append the full list of installers to enable batch downloading of installers. 796 | $Installers | ForEach-Object { 797 | $allInstallers += , $_ 798 | } 799 | } 800 | end { 801 | $downloads = @() 802 | 803 | try { 804 | $global:downloadData = [ordered]@{ } 805 | $downloadIndex = 1 806 | 807 | $allInstallers | ForEach-Object { 808 | $installerFileName = [io.Path]::GetFileName($_.DownloadUrl) 809 | $destination = [io.Path]::Combine($fullCachePath, "Installers", "Unity-$($_.Version)", "$installerFileName") 810 | 811 | # Already downloaded? 812 | if ( Test-Path $destination ) { 813 | $destinationItem = Get-Item $destination 814 | if ( ($destinationItem.Length -eq $_.Length ) -and 815 | ($destinationItem.LastWriteTime -eq $_.LastModified) ) { 816 | Write-Verbose "Skipping download because it's already in the cache: $($_.DownloadUrl)" 817 | 818 | $resource = New-Object UnitySetupResource -Property @{ 819 | 'ComponentType' = $_.ComponentType 820 | 'Path' = $destination 821 | } 822 | $downloads += , $resource 823 | return 824 | } 825 | } 826 | 827 | $destinationDirectory = [io.path]::GetDirectoryName($destination) 828 | if (!(Test-Path $destinationDirectory -PathType Container)) { 829 | New-Item "$destinationDirectory" -ItemType Directory | Out-Null 830 | } 831 | 832 | $webClient = New-Object System.Net.WebClient 833 | 834 | ++$downloadIndex 835 | $global:downloadData[$installerFileName] = New-Object PSObject -Property @{ 836 | installerFileName = $installerFileName 837 | startTime = Get-Date 838 | totalBytes = [int64]$_.Length 839 | receivedBytes = [int64]0 840 | isDownloaded = $false 841 | destination = $destination 842 | lastModified = $_.LastModified 843 | componentType = $_.ComponentType 844 | webClient = $webClient 845 | downloadIndex = $downloadIndex 846 | } 847 | 848 | # Register to events for showing progress of file download. 849 | Register-ObjectEvent -InputObject $webClient -EventName DownloadProgressChanged -SourceIdentifier "$installerFileName-Changed" -MessageData $installerFileName -Action { 850 | $global:downloadData[$event.MessageData].receivedBytes = $event.SourceArgs.BytesReceived 851 | } | Out-Null 852 | Register-ObjectEvent -InputObject $webClient -EventName DownloadFileCompleted -SourceIdentifier "$installerFileName-Completed" -MessageData $installerFileName -Action { 853 | $global:downloadData[$event.MessageData].isDownloaded = $true 854 | } | Out-Null 855 | 856 | try { 857 | Write-Verbose "Downloading $($_.DownloadUrl) to $destination" 858 | $webClient.DownloadFileAsync($_.DownloadUrl, $destination) 859 | } 860 | catch [System.Net.WebException] { 861 | Write-Error "Failed downloading $installerFileName - $($_.Exception.Message)" 862 | $global:downloadData.Remove($installerFileName) 863 | 864 | Unregister-Event -SourceIdentifier "$installerFileName-Completed" -Force 865 | Unregister-Event -SourceIdentifier "$installerFileName-Changed" -Force 866 | 867 | $webClient.Dispose() 868 | } 869 | } 870 | 871 | # Showing progress of all file downloads 872 | $totalDownloads = $global:downloadData.Count 873 | do { 874 | $installersDownloaded = 0 875 | 876 | $global:downloadData.Keys | ForEach-Object { 877 | $installerFileName = $_ 878 | $data = $global:downloadData[$installerFileName] 879 | 880 | # Finished downloading 881 | if ($null -eq $data.webClient) { 882 | ++$installersDownloaded 883 | return 884 | } 885 | if ($data.isDownloaded) { 886 | Write-Progress -Activity "Downloading $installerFileName" -Status "Done" -Completed ` 887 | -Id $data.downloadIndex 888 | 889 | Unregister-Event -SourceIdentifier "$installerFileName-Completed" -Force 890 | Unregister-Event -SourceIdentifier "$installerFileName-Changed" -Force 891 | 892 | $data.webClient.Dispose() 893 | $data.webClient = $null 894 | 895 | # Re-writes the last modified time for ensuring downloads are cached properly. 896 | $downloadedFile = Get-Item $data.destination 897 | $downloadedFile.LastWriteTime = $data.lastModified 898 | 899 | $resource = New-Object UnitySetupResource -Property @{ 900 | 'ComponentType' = $data.componentType 901 | 'Path' = $data.destination 902 | } 903 | $downloads += , $resource 904 | return 905 | } 906 | 907 | $elapsedTime = (Get-Date) - $data.startTime 908 | $progress = [int](($data.receivedBytes / [double]$data.totalBytes) * 100) 909 | $secondsRemaining = -1 # -1 for Write-Progress prevents seconds remaining from showing. 910 | 911 | if ($data.receivedBytes -gt 0 -and $elapsedTime.TotalSeconds -gt 0) { 912 | $averageSpeed = $data.receivedBytes / $elapsedTime.TotalSeconds 913 | $secondsRemaining = ($data.totalBytes - $data.receivedBytes) / $averageSpeed 914 | } 915 | 916 | $downloadSpeed = Format-BitsPerSecond -Bytes $data.receivedBytes -Seconds $elapsedTime.TotalSeconds 917 | 918 | Write-Progress -Activity "Downloading $installerFileName | $downloadSpeed" ` 919 | -Status "$($data.receivedBytes | Format-Bytes) of $($data.totalBytes | Format-Bytes)" ` 920 | -SecondsRemaining $secondsRemaining ` 921 | -PercentComplete $progress ` 922 | -Id $data.downloadIndex 923 | } 924 | } while ($installersDownloaded -lt $totalDownloads) 925 | } 926 | finally { 927 | # If the script is stopped, e.g. Ctrl+C, we want to cancel any remaining downloads 928 | $global:downloadData.Keys | ForEach-Object { 929 | $installerFileName = $_ 930 | $data = $global:downloadData[$installerFileName] 931 | 932 | if ($null -ne $data.webClient) { 933 | if (-not $data.isDownloaded) { 934 | $data.webClient.CancelAsync() 935 | } 936 | 937 | Unregister-Event -SourceIdentifier "$installerFileName-Completed" -Force 938 | Unregister-Event -SourceIdentifier "$installerFileName-Changed" -Force 939 | 940 | $data.webClient.Dispose() 941 | $data.webClient = $null 942 | } 943 | } 944 | 945 | Remove-Variable -Name downloadData -Scope Global 946 | } 947 | 948 | return $downloads 949 | } 950 | } 951 | 952 | function Install-UnitySetupPackage { 953 | [CmdletBinding()] 954 | param( 955 | [parameter(Mandatory = $true)] 956 | [UnitySetupResource] $Package, 957 | 958 | [parameter(Mandatory = $true)] 959 | [string]$Destination 960 | ) 961 | 962 | $currentOS = Get-OperatingSystem 963 | switch ($currentOS) { 964 | ([OperatingSystem]::Windows) { 965 | $startProcessArgs = @{ 966 | 'FilePath' = $Package.Path; 967 | 'ArgumentList' = @("/S", "/D=$Destination"); 968 | 'PassThru' = $true; 969 | 'Wait' = $true; 970 | } 971 | } 972 | ([OperatingSystem]::Linux) { 973 | throw "Install-UnitySetupPackage has not been implemented on the Linux platform. Contributions welcomed!"; 974 | } 975 | ([OperatingSystem]::Mac) { 976 | # Note that $Destination has to be a disk path. 977 | # sudo installer -package $Package.Path -target / 978 | $startProcessArgs = @{ 979 | 'FilePath' = 'sudo'; 980 | 'ArgumentList' = @("installer", "-package", $Package.Path, "-target", $Destination); 981 | 'PassThru' = $true; 982 | 'Wait' = $true; 983 | } 984 | } 985 | } 986 | 987 | Write-Verbose "$(Get-Date): Installing $($Package.ComponentType) to $Destination." 988 | $process = Start-Process @startProcessArgs 989 | if ( $process ) { 990 | if ( $process.ExitCode -ne 0) { 991 | Write-Error "$(Get-Date): Failed with exit code: $($process.ExitCode)" 992 | } 993 | else { 994 | Write-Verbose "$(Get-Date): Succeeded." 995 | } 996 | } 997 | } 998 | 999 | <# 1000 | .Synopsis 1001 | Installs a UnitySetup instance. 1002 | .DESCRIPTION 1003 | Downloads and installs UnitySetup installers found via Find-UnitySetupInstaller. 1004 | .PARAMETER Installers 1005 | What installers would you like to download and execute? 1006 | .PARAMETER BasePath 1007 | Under what base patterns is Unity customly installed at. 1008 | .PARAMETER Destination 1009 | Where would you like the UnitySetup instance installed? 1010 | .PARAMETER Cache 1011 | Where should the installers be cached. This defaults to ~\.unitysetup. 1012 | .EXAMPLE 1013 | Find-UnitySetupInstaller -Version 2017.3.0f3 | Install-UnitySetupInstance 1014 | .EXAMPLE 1015 | Find-UnitySetupInstaller -Version 2017.3.0f3 | Install-UnitySetupInstance -Destination D:\Unity-2017.3.0f3 1016 | .EXAMPLE 1017 | Find-UnitySetupInstaller -Version 2017.3.0f3 | Install-UnitySetupInstance -BasePath D:\UnitySetup\ 1018 | .EXAMPLE 1019 | Find-UnitySetupInstaller -Version 2017.3.0f3 | Install-UnitySetupInstance -BasePath D:\UnitySetup\ -Destination Unity-2017 1020 | #> 1021 | function Install-UnitySetupInstance { 1022 | [CmdletBinding()] 1023 | param( 1024 | [parameter(ValueFromPipeline = $true)] 1025 | [UnitySetupInstaller[]] $Installers, 1026 | 1027 | [parameter(Mandatory = $false)] 1028 | [string]$BasePath, 1029 | 1030 | [parameter(Mandatory = $false)] 1031 | [string]$Destination, 1032 | 1033 | [parameter(Mandatory = $false)] 1034 | [string]$Cache = [io.Path]::Combine("~", ".unitysetup") 1035 | ) 1036 | begin { 1037 | $currentOS = Get-OperatingSystem 1038 | if ($currentOS -eq [OperatingSystem]::Linux) { 1039 | throw "Install-UnitySetupInstance has not been implemented on the Linux platform. Contributions welcomed!"; 1040 | } 1041 | 1042 | if ( -not $PSBoundParameters.ContainsKey('BasePath') ) { 1043 | $defaultInstallPath = switch ($currentOS) { 1044 | ([OperatingSystem]::Windows) { 1045 | "C:\Program Files\Unity\Hub\Editor\" 1046 | } 1047 | ([OperatingSystem]::Linux) { 1048 | throw "Install-UnitySetupInstance has not been implemented on the Linux platform. Contributions welcomed!"; 1049 | } 1050 | ([OperatingSystem]::Mac) { 1051 | "/Applications/Unity/Hub/Editor/" 1052 | } 1053 | } 1054 | } 1055 | else { 1056 | $defaultInstallPath = $BasePath 1057 | } 1058 | 1059 | $versionInstallers = @{ } 1060 | } 1061 | process { 1062 | # Sort each installer received from the pipe into versions 1063 | $Installers | ForEach-Object { 1064 | $versionInstallers[$_.Version] += , $_ 1065 | } 1066 | } 1067 | end { 1068 | $versionInstallers.Keys | ForEach-Object { 1069 | $installVersion = $_ 1070 | $installerInstances = $versionInstallers[$installVersion] 1071 | 1072 | if ( $PSBoundParameters.ContainsKey('Destination') ) { 1073 | # Slight API change here. If BasePath is also provided treat Destination as a relative path. 1074 | if ( $PSBoundParameters.ContainsKey('BasePath') ) { 1075 | $installPath = $Destination 1076 | } 1077 | else { 1078 | $installPath = [io.path]::Combine($BasePath, $Destination) 1079 | } 1080 | } 1081 | else { 1082 | $installPath = [io.path]::Combine($defaultInstallPath, $installVersion) 1083 | } 1084 | 1085 | if ($currentOS -eq [OperatingSystem]::Mac) { 1086 | $volumeRoot = "/Volumes/UnitySetup/" 1087 | $volumeInstallPath = [io.path]::Combine($volumeRoot, "Applications/Unity/") 1088 | 1089 | # Make sure the install path ends with a trailing slash. This 1090 | # is required in some commands to treat as directory. 1091 | if (-not $installPath.EndsWith([io.path]::DirectorySeparatorChar)) { 1092 | $installPath += [io.path]::DirectorySeparatorChar 1093 | } 1094 | 1095 | # Make sure the folder .unitysetup exist before create sparsebundle 1096 | if (-not (Test-Path $Cache -PathType Container)) { 1097 | Write-Verbose "Creating directory $Cache." 1098 | New-Item $Cache -ItemType Directory -ErrorAction Stop | Out-Null 1099 | } 1100 | 1101 | # Creating sparse bundle to host installing Unity in other locations 1102 | $unitySetupBundlePath = [io.path]::Combine($Cache, "UnitySetup.sparsebundle") 1103 | if (-not (Test-Path $unitySetupBundlePath)) { 1104 | Write-Verbose "Creating new sparse bundle disk image for installation." 1105 | & hdiutil create -size 32g -fs 'HFS+' -type 'SPARSEBUNDLE' -volname 'UnitySetup' $unitySetupBundlePath 1106 | } 1107 | Write-Verbose "Mounting sparse bundle disk." 1108 | & hdiutil mount $unitySetupBundlePath 1109 | 1110 | # Previous version failed to remove. Cleaning up! 1111 | if (Test-Path $volumeInstallPath) { 1112 | Write-Verbose "Previous install did not clean up properly. Doing that now." 1113 | & sudo rm -Rf ([io.path]::Combine($volumeRoot, '*')) 1114 | } 1115 | 1116 | # Copy installed version back to the sparse bundle disk for Unity component installs. 1117 | if (Test-UnitySetupInstance -Path $installPath -BasePath $BasePath) { 1118 | Write-Verbose "Copying $installPath to $volumeInstallPath" 1119 | 1120 | # Ensure the path exists before copying the previous version to the sparse bundle disk. 1121 | & mkdir -p $volumeInstallPath 1122 | 1123 | # Copy the files (-r) and recreate symlinks (-l) to the install directory. 1124 | # Preserve permissions (-p) and owner (-o). 1125 | # Need to mark the files with read permissions or installs may fail. 1126 | & sudo rsync -rlpo $installPath $volumeInstallPath --chmod=+r 1127 | } 1128 | } 1129 | 1130 | # TODO: Strip out components already installed in the destination. 1131 | 1132 | $installerPaths = $installerInstances | Request-UnitySetupInstaller -Cache $Cache 1133 | 1134 | # First install the Unity editor before other components. 1135 | $editorComponent = switch ($currentOS) { 1136 | ([OperatingSystem]::Windows) { [UnitySetupComponent]::Windows } 1137 | ([OperatingSystem]::Linux) { [UnitySetupComponent]::Linux } 1138 | ([OperatingSystem]::Mac) { [UnitySetupComponent]::Mac } 1139 | } 1140 | 1141 | $packageDestination = $installPath 1142 | # Installers in macOS get installed to the sparse bundle disk first. 1143 | if ($currentOS -eq [OperatingSystem]::Mac) { 1144 | $packageDestination = $volumeRoot 1145 | } 1146 | 1147 | $editorInstaller = $installerPaths | Where-Object { $_.ComponentType -band $editorComponent } 1148 | if ($null -ne $editorInstaller) { 1149 | Write-Verbose "Installing $($editorInstaller.ComponentType) Editor" 1150 | Install-UnitySetupPackage -Package $editorInstaller -Destination $packageDestination 1151 | } 1152 | 1153 | $installerPaths | ForEach-Object { 1154 | # Already installed this earlier. Skipping. 1155 | if ($_.ComponentType -band $editorComponent) { 1156 | return 1157 | } 1158 | 1159 | Write-Verbose "Installing $($_.ComponentType)" 1160 | Install-UnitySetupPackage -Package $_ -Destination $packageDestination 1161 | } 1162 | 1163 | # Move the install from the sparse bundle disk to the install directory. 1164 | if ($currentOS -eq [OperatingSystem]::Mac) { 1165 | # rsync does not recursively create the directory path. 1166 | if (-not (Test-Path $installPath -PathType Container)) { 1167 | Write-Verbose "Creating directory $installPath." 1168 | New-Item $installPath -ItemType Directory -ErrorAction Stop | Out-Null 1169 | } 1170 | 1171 | Write-Verbose "Copying install to $installPath." 1172 | # Copy the files (-r) and recreate symlinks (-l) to the install directory. 1173 | # Preserve permissions (-p) and owner (-o). 1174 | # chmod gives files read permissions. 1175 | & sudo rsync -rlpo $volumeInstallPath $installPath --chmod="+wr" --remove-source-files 1176 | 1177 | Write-Verbose "Freeing sparse bundle disk space and unmounting." 1178 | # Ensure the drive is cleaned up. 1179 | & sudo rm -Rf ([io.path]::Combine($volumeRoot, '*')) 1180 | 1181 | & hdiutil eject $volumeRoot 1182 | # Free up disk space since deleting items in the volume send them to the trash 1183 | # Also note that -batteryallowed enables compacting while not connected to 1184 | # power. The compact is quite quick since the volume is small. 1185 | & hdiutil compact $unitySetupBundlePath -batteryallowed 1186 | } 1187 | } 1188 | } 1189 | } 1190 | 1191 | <# 1192 | .Synopsis 1193 | Uninstall Unity Setup Instances 1194 | .DESCRIPTION 1195 | Uninstall the specified Unity Setup Instances 1196 | .PARAMETER Instance 1197 | What instances of UnitySetup should be uninstalled 1198 | .EXAMPLE 1199 | Get-UnitySetupInstance | Uninstall-UnitySetupInstance 1200 | #> 1201 | function Uninstall-UnitySetupInstance { 1202 | [CmdletBinding(SupportsShouldProcess)] 1203 | param( 1204 | [parameter(Mandatory = $true, ValueFromPipeline = $true)] 1205 | [UnitySetupInstance[]] $Instances 1206 | ) 1207 | 1208 | process { 1209 | foreach ( $setupInstance in $Instances ) { 1210 | $uninstaller = Get-ChildItem "$($setupInstance.Path)" -Filter 'Uninstall.exe' -Recurse | 1211 | Select-Object -First 1 -ExpandProperty FullName 1212 | 1213 | if ($null -eq $uninstaller) { 1214 | Write-Error "Could not find Uninstaller.exe under $($setupInstance.Path)" 1215 | continue 1216 | } 1217 | 1218 | $startProcessArgs = @{ 1219 | 'FilePath' = $uninstaller; 1220 | 'PassThru' = $true; 1221 | 'Wait' = $true; 1222 | 'ErrorAction' = 'Stop'; 1223 | 'ArgumentList' = @("/S"); 1224 | } 1225 | 1226 | if ( -not $PSCmdlet.ShouldProcess("$uninstaller", "Start-Process")) { continue } 1227 | 1228 | $process = Start-Process @startProcessArgs 1229 | if ( $process.ExitCode -ne 0 ) { 1230 | Write-Error "Uninstaller quit with non-zero exit code" 1231 | } 1232 | } 1233 | } 1234 | } 1235 | 1236 | <# 1237 | .Synopsis 1238 | Get the Unity versions installed 1239 | .DESCRIPTION 1240 | Get the Unity versions installed and their locations 1241 | .PARAMETER BasePath 1242 | Under what base patterns should we look for Unity installs? 1243 | Defaults to Unity default locations by platform 1244 | Default can be configured by comma separated paths in $env:UNITY_SETUP_INSTANCE_DEFAULT_BASEPATH 1245 | .EXAMPLE 1246 | Get-UnitySetupInstance 1247 | #> 1248 | function Get-UnitySetupInstance { 1249 | [CmdletBinding()] 1250 | param( 1251 | [parameter(Mandatory = $false)] 1252 | [string[]] $BasePath 1253 | ) 1254 | 1255 | if((-not $BasePath) -and $env:UNITY_SETUP_INSTANCE_BASEPATH_DEFAULT){ 1256 | $BasePath = ($env:UNITY_SETUP_INSTANCE_BASEPATH_DEFAULT -split ',') | ForEach-Object { 1257 | $_.trim() 1258 | } 1259 | Write-Verbose "Set BasePath to $BasePath from `$env:UNITY_SETUP_INSTANCE_BASEPATH_DEFAULT." 1260 | } 1261 | 1262 | switch (Get-OperatingSystem) { 1263 | ([OperatingSystem]::Windows) { 1264 | if (-not $BasePath) { 1265 | $BasePath = @('C:\Program Files*\Unity*', 'C:\Program Files\Unity\Hub\Editor\*') 1266 | } 1267 | } 1268 | ([OperatingSystem]::Linux) { 1269 | throw "Get-UnitySetupInstance has not been implemented on the Linux platform. Contributions welcomed!"; 1270 | } 1271 | ([OperatingSystem]::Mac) { 1272 | if (-not $BasePath) { 1273 | $BasePath = @('/Applications/Unity*', '/Applications/Unity/Hub/Editor/*') 1274 | } 1275 | } 1276 | } 1277 | 1278 | Write-Verbose "Searching `"$BasePath`" for UnitySetup instances..." 1279 | Get-ChildItem -Path $BasePath -Directory -ErrorAction Ignore | 1280 | Where-Object { (Get-UnityEditor $_.FullName).Count -gt 0 } | 1281 | ForEach-Object { 1282 | $path = $_.FullName 1283 | try { 1284 | Write-Verbose "Creating UnitySetupInstance for $path" 1285 | [UnitySetupInstance]::new($path) 1286 | } 1287 | catch { 1288 | Write-Warning "$_" 1289 | } 1290 | } 1291 | } 1292 | 1293 | <# 1294 | .Synopsis 1295 | Gets the UnityVersion for a UnitySetupInstance at Path 1296 | .DESCRIPTION 1297 | Given a set of unity setup instances, this will select the best one matching your requirements 1298 | .PARAMETER Path 1299 | Path to a UnitySetupInstance 1300 | .OUTPUTS 1301 | UnityVersion 1302 | Returns the UnityVersion for the UnitySetupInstance at Path, or nothing if there isn't one 1303 | .EXAMPLE 1304 | Get-UnitySetupInstanceVersion -Path 'C:\Program Files\Unity' 1305 | #> 1306 | function Get-UnitySetupInstanceVersion { 1307 | [CmdletBinding()] 1308 | param( 1309 | [ValidateNotNullOrEmpty()] 1310 | [ValidateScript( { Test-Path $_ -PathType Container })] 1311 | [Parameter(Mandatory = $true, Position = 0)] 1312 | [string]$Path 1313 | ) 1314 | 1315 | Write-Verbose "Attempting to find UnityVersion in $path" 1316 | 1317 | # Try to look in the modules.json file for installer paths that contain version info 1318 | if ( Test-Path "$path\modules.json" -PathType Leaf ) { 1319 | try { 1320 | Write-Verbose "Searching $path\modules.json for module versions" 1321 | $table = (Get-Content "$path\modules.json" -Raw) | ConvertFrom-Json -AsHashtable 1322 | 1323 | foreach ( $url in $table.downloadUrl ) { 1324 | Write-Debug "`tTesting DownloadUrl $url" 1325 | if ( $url -notmatch "(\d+)\.(\d+)\.(\d+)([fpab])(\d+)" ) { continue; } 1326 | 1327 | Write-Verbose "`tFound version!" 1328 | return [UnityVersion]$Matches[0] 1329 | } 1330 | } 1331 | catch { 1332 | Write-Verbose "Error parsing $path\modules.json:`n`t$_" 1333 | } 1334 | } 1335 | 1336 | # No version found, start digging deeper 1337 | if ( Test-Path "$path\Editor" -PathType Container ) { 1338 | 1339 | # Search for the version using the ivy.xml definitions for legacy editor compatibility. 1340 | Write-Verbose "Looking for ivy.xml files under $path\Editor\" 1341 | $ivyFiles = Get-ChildItem -Path "$path\Editor\" -Filter 'ivy.xml' -Recurse -ErrorAction SilentlyContinue -Force -File 1342 | foreach ( $ivy in $ivyFiles) { 1343 | if ( $null -eq $ivy ) { continue; } 1344 | 1345 | Write-Verbose "`tLooking for version in $($ivy.FullName)" 1346 | 1347 | [xml]$xmlDoc = Get-Content $ivy.FullName 1348 | 1349 | [string]$ivyVersion = $xmlDoc.'ivy-module'.info.unityVersion 1350 | if ( -not $ivyVersion ) { continue; } 1351 | 1352 | Write-Verbose "`tFound version!" 1353 | return [UnityVersion]$ivyVersion 1354 | } 1355 | 1356 | # Search through any header files which might define the unity version 1357 | [string[]]$knownFiles = @( 1358 | "$path\Editor\Data\PlaybackEngines\windowsstandalonesupport\Source\WindowsPlayer\WindowsPlayer\UnityConfigureVersion.gen.h", 1359 | "$path\Editor\Data\PlaybackEngines\windowsstandalonesupport\Source\WindowsPlayer\WindowsPlayer\UnityConfiguration.gen.cpp" 1360 | ) 1361 | foreach ($file in $knownFiles) { 1362 | Write-Verbose "Looking for UNITY_VERSION defined in $file" 1363 | if (Test-Path -PathType Leaf -Path $file) { 1364 | $fileMatchInfo = Select-String -Path $file -Pattern "UNITY_VERSION.+`"(\d+\.\d+\.\d+[fpba]\d+).*`"" 1365 | if($null -ne $fileMatchInfo) 1366 | { 1367 | break; 1368 | } 1369 | } 1370 | } 1371 | 1372 | if ($null -eq $fileMatchInfo) { 1373 | Write-Verbose "Looking for source files with UNITY_VERSION defined under $path\Editor\ " 1374 | $fileMatchInfo = do { 1375 | Get-ChildItem -Path "$path\Editor" -Include '*.cpp','*.h' -Recurse -ErrorAction Ignore -Force -File | 1376 | Select-String -Pattern "UNITY_VERSION.+`"(\d+\.\d+\.\d+[fpba]\d+).*`"" | 1377 | ForEach-Object { $_; break; } # Stop the pipeline after the first result 1378 | } while ($false); 1379 | } 1380 | 1381 | if ( $fileMatchInfo.Matches.Groups.Count -gt 1 ) { 1382 | Write-Verbose "`tFound version!" 1383 | return [UnityVersion]($fileMatchInfo.Matches.Groups[1].Value) 1384 | } 1385 | } 1386 | } 1387 | 1388 | <# 1389 | .Synopsis 1390 | Selects a unity setup instance 1391 | .DESCRIPTION 1392 | Given a set of unity setup instances, this will select the best one matching your requirements 1393 | .PARAMETER Latest 1394 | Select the latest version available. 1395 | .PARAMETER Version 1396 | Select only instances matching Version. 1397 | .PARAMETER Path 1398 | Select only instances matching the project at the provided path. 1399 | .PARAMETER instances 1400 | The list of instances to Select from. 1401 | .EXAMPLE 1402 | Get-UnitySetupInstance | Select-UnitySetupInstance -Latest 1403 | .EXAMPLE 1404 | Get-UnitySetupInstance | Select-UnitySetupInstance -Version 2017.1.0f3 1405 | .EXAMPLE 1406 | Get-UnitySetupInstance | Select-UnitySetupInstance -Path (Get-Item /Applications/Unity*) 1407 | #> 1408 | function Select-UnitySetupInstance { 1409 | [CmdletBinding()] 1410 | param( 1411 | [parameter(Mandatory = $false)] 1412 | [switch] $Latest, 1413 | 1414 | [parameter(Mandatory = $false)] 1415 | [UnityVersion] $Version, 1416 | 1417 | [parameter(Mandatory = $false)] 1418 | [string] $Path, 1419 | 1420 | [parameter(Mandatory = $true, ValueFromPipeline = $true)] 1421 | [UnitySetupInstance[]] $Instances 1422 | ) 1423 | 1424 | begin { 1425 | if ( $Path ) { 1426 | $pathInfo = Resolve-Path $Path -ErrorAction Ignore 1427 | } 1428 | } 1429 | 1430 | process { 1431 | if ( $pathInfo ) { 1432 | $Instances = $Instances | Where-Object { 1433 | $instancePathInfo = Resolve-Path $_.Path -ErrorAction Ignore 1434 | return $pathInfo.Path -eq $instancePathInfo.Path 1435 | } 1436 | } 1437 | 1438 | if ( $Version ) { 1439 | $Instances = $Instances | Where-Object { [UnityVersion]::Compare($_.Version, $Version) -eq 0 } 1440 | } 1441 | 1442 | if ( $Latest ) { 1443 | foreach ( $i in $Instances ) { 1444 | if ( $null -eq $latestInstance -or [UnityVersion]::Compare($i.Version, $latestInstance.Version) -gt 0) { 1445 | $latestInstance = $i 1446 | } 1447 | } 1448 | } 1449 | elseif ( $Instances.Count -gt 0 ) { $Instances } 1450 | } 1451 | end { 1452 | if ($latestInstance) { $latestInstance } 1453 | } 1454 | } 1455 | 1456 | <# 1457 | .Synopsis 1458 | Get the Unity Projects under a specfied folder 1459 | .DESCRIPTION 1460 | Recursively discovers Unity projects and their Unity version 1461 | .PARAMETER BasePath 1462 | Under what base pattern should we look for Unity projects? Defaults to '$PWD'. 1463 | .EXAMPLE 1464 | Get-UnityProjectInstance 1465 | .EXAMPLE 1466 | Get-UnityProjectInstance -BasePath .\MyUnityProjects -Recurse 1467 | #> 1468 | function Get-UnityProjectInstance { 1469 | [CmdletBinding()] 1470 | param( 1471 | [parameter(Mandatory = $false)] 1472 | [string] $BasePath = $PWD, 1473 | 1474 | [parameter(Mandatory = $false)] 1475 | [switch] $Recurse 1476 | ) 1477 | 1478 | $args = @{ 1479 | 'Path' = $BasePath; 1480 | 'Filter' = 'ProjectSettings'; 1481 | 'ErrorAction' = 'Ignore'; 1482 | 'Directory' = $true; 1483 | } 1484 | 1485 | if ( $Recurse ) { 1486 | $args['Recurse'] = $true; 1487 | } 1488 | 1489 | Get-ChildItem @args | 1490 | ForEach-Object { 1491 | $path = [io.path]::Combine($_.FullName, "ProjectVersion.txt") 1492 | if ( Test-Path $path ) { 1493 | [UnityProjectInstance]::new((Join-Path $_.FullName "..\" | Convert-Path)) 1494 | } 1495 | } 1496 | } 1497 | 1498 | <# 1499 | .Synopsis 1500 | Tests the meta file integrity of the Unity Project Instance(s). 1501 | .DESCRIPTION 1502 | Tests if every item under assets has an associated .meta file 1503 | and every .meta file an associated item 1504 | and that none of the meta file guids collide. 1505 | .PARAMETER Project 1506 | Unity Project Instance(s) to test the meta file integrity of. 1507 | .PARAMETER PassThru 1508 | Output the meta file integrity issues rather than $true (no issues) or $false (at least one issue). 1509 | .EXAMPLE 1510 | Test-UnityProjectInstanceMetaFileIntegrity 1511 | .EXAMPLE 1512 | Test-UnityProjectInstanceMetaFileIntegrity -PassThru 1513 | .EXAMPLE 1514 | Test-UnityProjectInstanceMetaFileIntegrity .\MyUnityProject 1515 | .EXAMPLE 1516 | Test-UnityProjectInstanceMetaFileIntegrity -Project .\MyUnityProject 1517 | .EXAMPLE 1518 | Get-UnityProjectInstance -Recurse | Test-UnityProjectInstanceMetaFileIntegrity 1519 | .EXAMPLE 1520 | Get-UnityProjectInstance -Recurse | Test-UnityProjectInstanceMetaFileIntegrity -PassThru 1521 | #> 1522 | function Test-UnityProjectInstanceMetaFileIntegrity { 1523 | [CmdletBinding(DefaultParameterSetName = "Context")] 1524 | [OutputType([bool])] 1525 | param( 1526 | [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0, ParameterSetName = "Projects")] 1527 | [ValidateNotNullOrEmpty()] 1528 | [UnityProjectInstance[]] $Project, 1529 | [switch] $PassThru 1530 | ) 1531 | 1532 | process { 1533 | 1534 | switch ( $PSCmdlet.ParameterSetName) { 1535 | 'Context' { 1536 | $currentFolderProject = Get-UnityProjectInstance $PWD.Path 1537 | if ($null -ne $currentFolderProject) { 1538 | $Project = @($currentFolderProject) 1539 | } 1540 | } 1541 | } 1542 | 1543 | # Derived from https://docs.unity3d.com/Manual/SpecialFolders.html 1544 | $unityAssetExcludes = @('.*', '*~', 'cvs', '*.tmp') 1545 | 1546 | foreach ( $p in $Project) { 1547 | 1548 | $testResult = $true 1549 | 1550 | Write-Verbose "Getting meta file integrity for project at $($p.path)" 1551 | $assetDir = Join-Path $p.Path "Assets" 1552 | 1553 | # get all the directories under assets 1554 | [System.IO.DirectoryInfo[]]$dirs = 1555 | Get-ChildItem -Path "$assetDir/*" -Recurse -Directory -Exclude $unityAssetExcludes 1556 | 1557 | Write-Verbose "Testing asset directories for missing meta files..." 1558 | [float]$progressCounter = 0 1559 | foreach ($dir in $dirs) { 1560 | 1561 | ++$progressCounter 1562 | $progress = @{ 1563 | 'Activity' = "Testing directories for missing meta files" 1564 | 'Status' = "$progressCounter / $($dirs.Length) - $dir" 1565 | 'PercentComplete' = (($progressCounter / $dirs.Length) * 100) 1566 | } 1567 | Write-Debug $progress.Status 1568 | Write-Progress @progress 1569 | 1570 | $testPath = "$($dir.FullName).meta"; 1571 | if (Test-Path -PathType Leaf -Path $testPath) { continue } 1572 | 1573 | if ($PassThru) { 1574 | [PSCustomObject]@{ 1575 | 'Item' = $dir 1576 | 'Issue' = "Directory is missing associated meta file." 1577 | } 1578 | } 1579 | else { 1580 | $testResult = $false; 1581 | break; 1582 | } 1583 | } 1584 | 1585 | if (-not $testResult) { $false; continue; } 1586 | 1587 | # get all the non-meta files under assets 1588 | $unityAssetFileExcludes = $unityAssetExcludes + '*.meta' 1589 | [System.IO.FileInfo[]]$files = Get-ChildItem -Path "$assetDir/*" -Exclude $unityAssetFileExcludes -File 1590 | foreach ($dir in $dirs) { 1591 | $files += Get-ChildItem -Path "$($dir.FullName)/*" -Exclude $unityAssetFileExcludes -File 1592 | } 1593 | 1594 | Write-Verbose "Testing asset files for missing meta files..." 1595 | $progressCounter = 0 1596 | foreach ( $file in $files ) { 1597 | 1598 | ++$progressCounter 1599 | $progress = @{ 1600 | 'Activity' = "Testing files for missing meta files" 1601 | 'Status' = "$progressCounter / $($files.Length) - $file" 1602 | 'PercentComplete' = (($progressCounter / $files.Length) * 100) 1603 | 1604 | } 1605 | Write-Debug $progress.Status 1606 | Write-Progress @progress 1607 | 1608 | $testPath = "$($file.FullName).meta"; 1609 | if (Test-Path -PathType Leaf -Path $testPath) { continue } 1610 | 1611 | if ($PassThru) { 1612 | [PSCustomObject]@{ 1613 | 'Item' = $file 1614 | 'Issue' = "File is missing associated meta file." 1615 | } 1616 | } 1617 | else { 1618 | $testResult = $false; 1619 | break; 1620 | } 1621 | } 1622 | 1623 | if (-not $testResult) { $false; continue; } 1624 | 1625 | $metaFileSearchArgs = @{ 1626 | 'Exclude' = $unityAssetExcludes 1627 | 'Include' = '*.meta' 1628 | 'File' = $true 1629 | 'Force' = $true # Ensure we include hidden meta files 1630 | } 1631 | 1632 | # get all the meta files under assets 1633 | [System.IO.FileInfo[]]$metaFiles = Get-ChildItem -Path "$assetDir/*" @metaFileSearchArgs 1634 | foreach ($dir in $dirs) { 1635 | $metaFiles += Get-ChildItem -Path "$($dir.FullName)/*" @metaFileSearchArgs 1636 | } 1637 | 1638 | Write-Verbose "Testing meta files for missing assets..." 1639 | $progressCounter = 0 1640 | foreach ($metaFile in $metaFiles) { 1641 | 1642 | ++$progressCounter 1643 | $progress = @{ 1644 | 'Activity' = "Testing meta files for missing assets" 1645 | 'Status' = "$progressCounter / $($metaFiles.Length) - $metaFile" 1646 | 'PercentComplete' = (($progressCounter / $metaFiles.Length) * 100) 1647 | } 1648 | Write-Debug $progress.Status 1649 | Write-Progress @progress 1650 | 1651 | $testPath = $metaFile.FullName.SubString(0, $metaFile.FullName.Length - $metaFile.Extension.Length); 1652 | if (Test-Path -Path $testPath) { continue } 1653 | 1654 | if ($PassThru) { 1655 | [PSCustomObject]@{ 1656 | 'Item' = $metaFile 1657 | 'Issue' = "Meta file is missing associated item." 1658 | } 1659 | } 1660 | else { 1661 | $testResult = $false; 1662 | break; 1663 | } 1664 | } 1665 | 1666 | if (-not $testResult) { $false; continue; } 1667 | 1668 | Write-Verbose "Testing meta files for guid collisions..." 1669 | $metaGuids = @{ } 1670 | $progressCounter = 0 1671 | foreach ($metaFile in $metaFiles) { 1672 | 1673 | ++$progressCounter 1674 | $progress = @{ 1675 | 'Activity' = "Testing meta files for guid collisions" 1676 | 'Status' = "$progressCounter / $($metaFiles.Length) - $metaFile" 1677 | 'PercentComplete' = (($progressCounter / $metaFiles.Length) * 100) 1678 | } 1679 | Write-Debug $progress.Status 1680 | Write-Progress @progress 1681 | 1682 | try { 1683 | $guidResult = Get-Content $metaFile.FullName | Select-String -Pattern '^guid:\s*([a-z,A-Z,\d]+)\s*$' 1684 | if ($guidResult.Matches.Groups.Length -lt 2) { 1685 | Write-Warning "Could not find guid in meta file - $metaFile" 1686 | continue; 1687 | } 1688 | 1689 | $guid = $guidResult.Matches.Groups[1].Value 1690 | if ($null -eq $metaGuids[$guid]) { 1691 | $metaGuids[$guid] = $metaFile; 1692 | continue 1693 | } 1694 | 1695 | if ($PassThru) { 1696 | [PSCustomObject]@{ 1697 | 'Item' = $metaFile 1698 | 'Issue' = "Meta file guid collision with $($metaGuids[$guid])" 1699 | } 1700 | } 1701 | else { 1702 | $testResult = $false; 1703 | break; 1704 | } 1705 | } 1706 | catch { 1707 | Write-Error "Exception testing guid of $metaFile - $_" 1708 | } 1709 | } 1710 | 1711 | if (-not $PassThru) { $testResult; } 1712 | } 1713 | } 1714 | } 1715 | 1716 | <# 1717 | .Synopsis 1718 | Starts the Unity Editor 1719 | .DESCRIPTION 1720 | If Project, Instance, and Latest are unspecified, tests if the current folder is a 1721 | UnityProjectInstance, and if so, selects it as Project. Otherwise the latest 1722 | UnitySetupInstance is selected as Instance. 1723 | .PARAMETER Project 1724 | The project instance to open the Unity Editor for. 1725 | .PARAMETER Setup 1726 | The setup instances to launch. If unspecified, the version at Project is selected. 1727 | .PARAMETER Latest 1728 | Launch the latest version installed. 1729 | .PARAMETER Version 1730 | Launch the specified version. 1731 | .PARAMETER IgnoreProjectContext 1732 | Force operation as though $PWD is not a unity project. 1733 | .PARAMETER ExecuteMethod 1734 | The script method for the Unity Editor to execute. 1735 | .PARAMETER AdditionalArguments 1736 | Additional arguments for Unity or your custom method 1737 | .PARAMETER OutputPath 1738 | The output path that the Unity Editor should use. 1739 | .PARAMETER LogFile 1740 | The log file for the Unity Editor to write to. 1741 | .PARAMETER BuildTarget 1742 | The platform build target for the Unity Editor to start in. 1743 | .PARAMETER StandaloneBuildSubtarget 1744 | Select an active build sub-target for the Standalone platforms before loading a project. 1745 | .PARAMETER AcceptAPIUpdate 1746 | Accept the API Updater automatically. Implies BatchMode unless explicitly specified by the user. 1747 | .PARAMETER Credential 1748 | What user name and password should be used by Unity for activation? 1749 | .PARAMETER Serial 1750 | What serial should be used by Unity for activation? Implies BatchMode and Quit if they're not supplied by the User. 1751 | .PARAMETER ReturnLicense 1752 | Unity should return the current license it's been activated with. Implies Quit if not supplied by the User. 1753 | .PARAMETER EditorTestsCategories 1754 | Filter tests by category names. 1755 | .PARAMETER EditorTestsFilter 1756 | Filter tests by test names. 1757 | .PARAMETER EditorTestsResultFile 1758 | Where to put the results? Unity states, "If the path is a folder, the command line uses a default file name. If not specified, it places the results in the project’s root folder." 1759 | .PARAMETER RunEditorTests 1760 | Should Unity run the editor tests? Unity states, "[...]it’s good practice to run it with batchmode argument. quit is not required, because the Editor automatically closes down after the run is finished." 1761 | .PARAMETER TestPlatform 1762 | The platform you want to run the tests on. Note that If unspecified, tests run in editmode by default. 1763 | .PARAMETER TestResults 1764 | The path indicating where Unity should save the result file. By default, Unity saves it in the Project’s root folder. 1765 | .PARAMETER RunTests 1766 | Should Unity run tests? Unity states, "[...]it’s good practice to run it with batchmode argument. quit is not required, because the Editor automatically closes down after the run is finished." 1767 | .PARAMETER BatchMode 1768 | Should the Unity Editor start in batch mode? 1769 | .PARAMETER Quit 1770 | Should the Unity Editor quit after it's done? 1771 | .PARAMETER Wait 1772 | Should the command wait for the Unity Editor to exit? 1773 | .PARAMETER CacheServerEndpoint 1774 | If included, the editor will attempt to use a Unity Accelerator hosted in the provided IP. The endpoint should be in the format of [IP]:[Port]. If the default Accelerator port is used, at the time of writing this, the port should be ommited. 1775 | .PARAMETER CacheServerNamespacePrefix 1776 | Set the namespace prefix. Used to group data together on the cache server. 1777 | .PARAMETER CacheServerDisableDownload 1778 | Disable downloading from the cache server. If ommited, the default value is true (download enabled) 1779 | .PARAMETER CacheServerDisableUpload 1780 | Disable uploading to the cache server. If ommited, the default value is true (upload enabled) 1781 | .EXAMPLE 1782 | Start-UnityEditor 1783 | .EXAMPLE 1784 | Start-UnityEditor -Latest 1785 | .EXAMPLE 1786 | Start-UnityEditor -Version 2017.3.0f3 1787 | .EXAMPLE 1788 | Start-UnityEditor -ExecuteMethod Build.Invoke -BatchMode -Quit -LogFile .\build.log -Wait -AdditionalArguments "-BuildArg1 -BuildArg2" 1789 | .EXAMPLE 1790 | Get-UnityProjectInstance -Recurse | Start-UnityEditor -BatchMode -Quit 1791 | .EXAMPLE 1792 | Get-UnitySetupInstance | Start-UnityEditor 1793 | #> 1794 | function Start-UnityEditor { 1795 | [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = "Context")] 1796 | param( 1797 | [parameter(Mandatory = $false, ValueFromPipeline = $true, ParameterSetName = 'Projects', Position = 0)] 1798 | [parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'ProjectsLatest', Position = 0)] 1799 | [parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'ProjectsVersion', Position = 0)] 1800 | [ValidateNotNullOrEmpty()] 1801 | [UnityProjectInstance[]] $Project, 1802 | [parameter(Mandatory = $false, ValueFromPipeline = $true, ParameterSetName = 'Setups')] 1803 | [ValidateNotNullOrEmpty()] 1804 | [UnitySetupInstance[]]$Setup, 1805 | [parameter(Mandatory = $true, ParameterSetName = 'Latest')] 1806 | [parameter(Mandatory = $true, ParameterSetName = 'ProjectsLatest')] 1807 | [switch]$Latest, 1808 | [parameter(Mandatory = $true, ParameterSetName = 'Version')] 1809 | [parameter(Mandatory = $true, ParameterSetName = 'ProjectsVersion')] 1810 | [UnityVersion]$Version, 1811 | [parameter(Mandatory = $false, ParameterSetName = 'Latest')] 1812 | [parameter(Mandatory = $false, ParameterSetName = 'Version')] 1813 | [parameter(Mandatory = $false, ParameterSetName = 'Context')] 1814 | [switch]$IgnoreProjectContext, 1815 | [parameter(Mandatory = $false)] 1816 | [string]$ExecuteMethod, 1817 | [parameter(Mandatory = $false)] 1818 | [string]$AdditionalArguments, 1819 | [parameter(Mandatory = $false)] 1820 | [string[]]$ExportPackage, 1821 | [parameter(Mandatory = $false)] 1822 | [string]$ImportPackage, 1823 | [parameter(Mandatory = $false)] 1824 | [string]$CreateProject, 1825 | [parameter(Mandatory = $false)] 1826 | [string]$OutputPath, 1827 | [parameter(Mandatory = $false)] 1828 | [string]$LogFile, 1829 | [parameter(Mandatory = $false)] 1830 | [ValidateSet('StandaloneOSX', 'StandaloneWindows', 'iOS', 'Android', 'StandaloneLinux', 'StandaloneWindows64', 'WebGL', 'WSAPlayer', 'StandaloneLinux64', 'StandaloneLinuxUniversal', 'Tizen', 'PSP2', 'PS4', 'XBoxOne', 'N3DS', 'WiiU', 'tvOS', 'Switch', 'Lumin')] 1831 | [string]$BuildTarget, 1832 | [parameter(Mandatory = $false)] 1833 | [ValidateSet('Player', 'Server')] 1834 | [string]$StandaloneBuildSubtarget, 1835 | [parameter(Mandatory = $false)] 1836 | [switch]$AcceptAPIUpdate, 1837 | [parameter(Mandatory = $false)] 1838 | [pscredential]$Credential, 1839 | [parameter(Mandatory = $false)] 1840 | [securestring]$Serial, 1841 | [parameter(Mandatory = $false)] 1842 | [switch]$ReturnLicense, 1843 | [parameter(Mandatory = $false)] 1844 | [switch]$ForceFree, 1845 | [parameter(Mandatory = $false)] 1846 | [string[]]$EditorTestsCategory, 1847 | [parameter(Mandatory = $false)] 1848 | [string[]]$EditorTestsFilter, 1849 | [parameter(Mandatory = $false)] 1850 | [string]$EditorTestsResultFile, 1851 | [parameter(Mandatory = $false)] 1852 | [switch]$RunEditorTests, 1853 | [parameter(Mandatory = $false)] 1854 | [ValidateSet('EditMode', 'PlayMode')] 1855 | [string]$TestPlatform, 1856 | [parameter(Mandatory = $false)] 1857 | [string]$TestResults, 1858 | [parameter(Mandatory = $false)] 1859 | [switch]$RunTests, 1860 | [parameter(Mandatory = $false)] 1861 | [switch]$BatchMode, 1862 | [parameter(Mandatory = $false)] 1863 | [switch]$Quit, 1864 | [parameter(Mandatory = $false)] 1865 | [switch]$Wait, 1866 | [parameter(Mandatory = $false)] 1867 | [switch]$PassThru, 1868 | [parameter(Mandatory = $false)] 1869 | [string]$CacheServerEndpoint, 1870 | [parameter(Mandatory = $false)] 1871 | [string]$CacheServerNamespacePrefix, 1872 | [parameter(Mandatory = $false)] 1873 | [switch]$CacheServerDisableDownload, 1874 | [parameter(Mandatory = $false)] 1875 | [switch]$CacheServerDisableUpload 1876 | ) 1877 | process { 1878 | switch -wildcard ( $PSCmdlet.ParameterSetName ) { 1879 | 'Context' { 1880 | $projectInstances = [UnityProjectInstance[]]@() 1881 | $setupInstances = [UnitySetupInstance[]]@() 1882 | 1883 | $currentFolderProject = if ( !$IgnoreProjectContext ) { Get-UnityProjectInstance $PWD.Path } 1884 | if ($null -ne $currentFolderProject) { 1885 | $projectInstances += , $currentFolderProject 1886 | } 1887 | else { 1888 | $setupInstance = Get-UnitySetupInstance | Select-UnitySetupInstance -Latest 1889 | if ($setupInstance.Count -gt 0) { 1890 | $setupInstances += , $setupInstance 1891 | } 1892 | } 1893 | } 1894 | 'Projects*' { 1895 | $projectInstances = $Project 1896 | $setupInstances = [UnitySetupInstance[]]@() 1897 | } 1898 | 'Setups' { 1899 | $projectInstances = [UnityProjectInstance[]]@() 1900 | $setupInstances = $Setup 1901 | } 1902 | 'Latest' { 1903 | $projectInstances = [UnityProjectInstance[]]@() 1904 | 1905 | $currentFolderProject = if (!$IgnoreProjectContext) { Get-UnityProjectInstance $PWD.Path } 1906 | if ($null -ne $currentFolderProject) { 1907 | $projectInstances += , $currentFolderProject 1908 | } 1909 | elseif ( $Latest ) { 1910 | $setupInstance = Get-UnitySetupInstance | Select-UnitySetupInstance -Latest 1911 | if ($setupInstance.Count -gt 0) { 1912 | $setupInstances = , $setupInstance 1913 | } 1914 | } 1915 | } 1916 | 'Version' { 1917 | $projectInstances = [UnityProjectInstance[]]@() 1918 | 1919 | $currentFolderProject = if (!$IgnoreProjectContext) { Get-UnityProjectInstance $PWD.Path } 1920 | if ($null -ne $currentFolderProject) { 1921 | $projectInstances += , $currentFolderProject 1922 | } 1923 | elseif ($null -ne $Version) { 1924 | $setupInstance = Get-UnitySetupInstance | Select-UnitySetupInstance -Version $Version 1925 | if ($setupInstance.Count -gt 0) { 1926 | $setupInstances = , $setupInstance 1927 | } 1928 | else { 1929 | Write-Error "Could not find Unity Editor for version $Version" 1930 | } 1931 | } 1932 | } 1933 | } 1934 | 1935 | [string[]]$sharedArgs = @() 1936 | if ( $ReturnLicense ) { 1937 | if ( -not $PSBoundParameters.ContainsKey('BatchMode') ) { $BatchMode = $true } 1938 | if ( -not $PSBoundParameters.ContainsKey('Quit') ) { $Quit = $true } 1939 | 1940 | $sharedArgs += '-returnLicense' 1941 | } 1942 | if ( $Serial ) { 1943 | if ( -not $PSBoundParameters.ContainsKey('BatchMode') ) { $BatchMode = $true } 1944 | if ( -not $PSBoundParameters.ContainsKey('Quit') ) { $Quit = $true } 1945 | } 1946 | if ( $AcceptAPIUpdate ) { 1947 | $sharedArgs += '-accept-apiupdate' 1948 | if ( -not $PSBoundParameters.ContainsKey('BatchMode')) { $BatchMode = $true } 1949 | } 1950 | if ( $CreateProject ) { $sharedArgs += "-createProject", "`"$CreateProject`"" } 1951 | if ( $ExecuteMethod ) { $sharedArgs += "-executeMethod", $ExecuteMethod } 1952 | if ( $OutputPath ) { $sharedArgs += "-buildOutput", "`"$OutputPath`"" } 1953 | if ( $LogFile ) { $sharedArgs += "-logFile", "`"$LogFile`"" } 1954 | if ( $BuildTarget ) { $sharedArgs += "-buildTarget", $BuildTarget } 1955 | if ( $StandaloneBuildSubtarget ) { $sharedArgs += "-standaloneBuildSubtarget", $StandaloneBuildSubtarget } 1956 | if ( $BatchMode ) { $sharedArgs += "-batchmode" } 1957 | if ( $Quit ) { $sharedArgs += "-quit" } 1958 | if ( $ExportPackage ) { $sharedArgs += "-exportPackage", ($ExportPackage | ForEach-Object { "`"$_`"" }) } 1959 | if ( $ImportPackage ) { $sharedArgs += "-importPackage", "`"$ImportPackage`"" } 1960 | if ( $Credential ) { $sharedArgs += '-username', $Credential.UserName } 1961 | if ( $EditorTestsCategory ) { $sharedArgs += '-editorTestsCategories', ($EditorTestsCategory -join ',') } 1962 | if ( $EditorTestsFilter ) { $sharedArgs += '-editorTestsFilter', ($EditorTestsFilter -join ',') } 1963 | if ( $EditorTestsResultFile ) { $sharedArgs += '-editorTestsResultFile', $EditorTestsResultFile } 1964 | if ( $RunEditorTests ) { $sharedArgs += '-runEditorTests' } 1965 | if ( $TestPlatform ) { $sharedArgs += '-testPlatform', $TestPlatform } 1966 | if ( $TestResults ) { $sharedArgs += '-testResults', $TestResults } 1967 | if ( $RunTests ) { $sharedArgs += '-runTests' } 1968 | if ( $ForceFree) { $sharedArgs += '-force-free' } 1969 | if ( $AdditionalArguments) { $sharedArgs += $AdditionalArguments } 1970 | if ( $CacheServerEndpoint) { 1971 | $sharedArgs += "-cacheServerEndpoint", $CacheServerEndpoint 1972 | $sharedArgs += "-adb2" 1973 | $sharedArgs += "-enableCacheServer" 1974 | if ( $CacheServerNamespacePrefix) { $sharedArgs += "-cacheServerNamespacePrefix", $CacheServerNamespacePrefix} 1975 | $sharedArgs += "-cacheServerEnableDownload", $(If ($CacheServerDisableDownload) {"false"} Else {"true"}) 1976 | $sharedArgs += "-cacheServerEnableUpload", $(If ($CacheServerDisableUpload) {"false"} Else {"true"}) 1977 | } 1978 | 1979 | [string[][]]$instanceArgs = @() 1980 | foreach ( $p in $projectInstances ) { 1981 | 1982 | if ( $Latest ) { 1983 | $setupInstance = Get-UnitySetupInstance | Select-UnitySetupInstance -Latest 1984 | if ($setupInstance.Count -eq 0) { 1985 | Write-Error "Could not find any Unity Editor installed" 1986 | continue 1987 | } 1988 | } 1989 | elseif ($null -ne $Version) { 1990 | $setupInstance = Get-UnitySetupInstance | Select-UnitySetupInstance -Version $Version 1991 | if ($setupInstance.Count -eq 0) { 1992 | Write-Error "Could not find Unity Editor for version $Version" 1993 | continue 1994 | } 1995 | } 1996 | else { 1997 | $setupInstance = Get-UnitySetupInstance | Select-UnitySetupInstance -Version $p.Version 1998 | if ($setupInstance.Count -eq 0) { 1999 | Write-Error "Could not find Unity Editor for version $($p.Version)" 2000 | continue 2001 | } 2002 | } 2003 | 2004 | $projectPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($($p.Path)) 2005 | $instanceArgs += , ("-projectPath", "`"$projectPath`"") 2006 | $setupInstances += , $setupInstance 2007 | } 2008 | 2009 | 2010 | for ($i = 0; $i -lt $setupInstances.Length; $i++) { 2011 | $setupInstance = $setupInstances[$i] 2012 | 2013 | $editor = Get-UnityEditor "$($setupInstance.Path)" 2014 | if ( -not $editor ) { 2015 | Write-Error "Could not find Unity Editor under setup instance path: $($setupInstance.Path)" 2016 | continue 2017 | } 2018 | 2019 | # clone the shared args list 2020 | [string[]]$unityArgs = $sharedArgs | ForEach-Object { $_ } 2021 | if ( $instanceArgs[$i] ) { $unityArgs += $instanceArgs[$i] } 2022 | 2023 | $actionString = "$editor $unityArgs" 2024 | if ( $Credential ) { $actionString += " -password (hidden)" } 2025 | if ( $Serial ) { $actionString += " -serial (hidden)" } 2026 | 2027 | if (-not $PSCmdlet.ShouldProcess($actionString, "System.Diagnostics.Process.Start()")) { 2028 | continue 2029 | } 2030 | 2031 | # Defered till after potential display by ShouldProcess 2032 | if ( $Credential ) { $unityArgs += '-password', $Credential.GetNetworkCredential().Password } 2033 | if ( $Serial ) { $unityArgs += '-serial', [System.Net.NetworkCredential]::new($null, $Serial).Password } 2034 | 2035 | # We've experienced issues with Start-Process -Wait and redirecting 2036 | # output so we're using the Process class directly now. 2037 | $process = New-Object System.Diagnostics.Process 2038 | $process.StartInfo.Filename = $editor 2039 | $process.StartInfo.Arguments = $unityArgs 2040 | $process.StartInfo.RedirectStandardOutput = $true 2041 | $process.StartInfo.RedirectStandardError = $true 2042 | $process.StartInfo.UseShellExecute = $false 2043 | $process.StartInfo.CreateNoWindow = $true 2044 | $process.StartInfo.WorkingDirectory = $PWD 2045 | $process.StartInfo.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden 2046 | $process.Start() | Out-Null 2047 | 2048 | if ( $Wait ) { 2049 | $process.WaitForExit() 2050 | 2051 | if ( $LogFile -and (Test-Path $LogFile -Type Leaf) ) { 2052 | # Note that Unity sometimes returns a success ExitCode despite the presence of errors, but we want 2053 | # to make sure that we flag such errors. 2054 | Write-UnityError $LogFile 2055 | 2056 | Write-Verbose "Writing $LogFile to Information stream Tagged as 'Logs'" 2057 | Get-Content $LogFile | ForEach-Object { Write-Information -MessageData $_ -Tags 'Logs' } 2058 | } 2059 | 2060 | if ( $process.ExitCode -ne 0 ) { 2061 | Write-Error "Unity quit with non-zero exit code: $($process.ExitCode)" 2062 | } 2063 | } 2064 | 2065 | if ($PassThru) { $process } 2066 | } 2067 | } 2068 | } 2069 | 2070 | # Open the specified Unity log file and write any errors found in the file to the error stream. 2071 | function Write-UnityError { 2072 | param([string] $LogFileName) 2073 | Write-Verbose "Checking $LogFileName for errors" 2074 | $errors = Get-Content $LogFileName | Where-Object { Get-IsUnityError $_ } 2075 | if ( $errors.Count -gt 0 ) { 2076 | $errors = $errors | Select-Object -uniq # Unity prints out errors as they occur and also in a summary list. We only want to see each unique error once. 2077 | $errorMessage = $errors -join "`r`n" 2078 | $errorMessage = "Errors were found in $LogFileName`:`r`n$errorMessage" 2079 | Write-Error $errorMessage 2080 | } 2081 | } 2082 | 2083 | function Get-IsUnityError { 2084 | param([string] $LogLine) 2085 | 2086 | # Detect Unity License error, for example: 2087 | # BatchMode: Unity has not been activated with a valid License. Could be a new activation or renewal... 2088 | if ( $LogLine -match 'Unity has not been activated with a valid License' ) { 2089 | return $true 2090 | } 2091 | 2092 | # Detect that the method specified by -ExecuteMethod doesn't exist, for example: 2093 | # executeMethod method 'Invoke' in class 'Build' could not be found. 2094 | if ( $LogLine -match 'executeMethod method .* could not be found' ) { 2095 | return $true 2096 | } 2097 | 2098 | # Detect compilation error, for example: 2099 | # Assets/Errors.cs(7,9): error CS0103: The name `NonexistentFunction' does not exist in the current context 2100 | if ( $LogLine -match '\.cs\(\d+,\d+\): error ' ) { 2101 | return $true 2102 | } 2103 | 2104 | # In the future, additional kinds of errors that can be found in Unity logs could be added here: 2105 | # ... 2106 | 2107 | return $false 2108 | } 2109 | 2110 | function ConvertTo-DateTime { 2111 | param([string] $Text) 2112 | 2113 | if ( -not $text -or $text.Length -eq 0 ) { [DateTime]::MaxValue } 2114 | else { [DateTime]$Text } 2115 | } 2116 | 2117 | <# 2118 | .Synopsis 2119 | Get the active Unity licenses for the machine. 2120 | .PARAMETER Serial 2121 | Filter licenses to the specified serial 2122 | .EXAMPLE 2123 | Get-UnityLicense 2124 | #> 2125 | function Get-UnityLicense { 2126 | [CmdletBinding()] 2127 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "", Justification = "Used to convert discovered plaintext serials into secure strings.")] 2128 | param([SecureString]$Serial) 2129 | 2130 | $licenseFiles = Get-ChildItem "C:\ProgramData\Unity\Unity_*.ulf" -ErrorAction 'SilentlyContinue' 2131 | foreach ( $licenseFile in $licenseFiles ) { 2132 | Write-Verbose "Discovered License File at $licenseFile" 2133 | $doc = [xml](Get-Content "$licenseFile") 2134 | $devBytes = [System.Convert]::FromBase64String($doc.root.License.DeveloperData.Value) 2135 | 2136 | # The first four bytes look like a count so skip that to pull out the serial string 2137 | $licenseSerial = [String]::new($devBytes[4..($devBytes.Length - 1)]) 2138 | if ( $Serial -and [System.Net.NetworkCredential]::new($null, $Serial).Password -ne $licenseSerial ) { continue; } 2139 | 2140 | $license = $doc.root.License 2141 | [PSCustomObject]@{ 2142 | 'LicenseVersion' = $license.LicenseVersion.Value 2143 | 'Serial' = ConvertTo-SecureString $licenseSerial -AsPlainText -Force 2144 | 'UnityVersion' = $license.ClientProvidedVersion.Value 2145 | 'DisplaySerial' = $license.SerialMasked.Value 2146 | 'ActivationDate' = ConvertTo-DateTime $license.InitialActivationDate.Value 2147 | 'StartDate' = ConvertTo-DateTime $license.StartDate.Value 2148 | 'StopDate' = ConvertTo-DateTime $license.StopDate.Value 2149 | 'UpdateDate' = ConvertTo-DateTime $license.UpdateDate.Value 2150 | } 2151 | } 2152 | } 2153 | 2154 | @( 2155 | @{ 'Name' = 'gusi'; 'Value' = 'Get-UnitySetupInstance' }, 2156 | @{ 'Name' = 'gupi'; 'Value' = 'Get-UnityProjectInstance' }, 2157 | @{ 'Name' = 'susi'; 'Value' = 'Select-UnitySetupInstance' }, 2158 | @{ 'Name' = 'gue'; 'Value' = 'Get-UnityEditor' } 2159 | @{ 'Name' = 'sue'; 'Value' = 'Start-UnityEditor' } 2160 | ) | ForEach-Object { 2161 | 2162 | $alias = Get-Alias -Name $_.Name -ErrorAction 'SilentlyContinue' 2163 | if ( -not $alias ) { 2164 | Write-Verbose "Creating new alias $($_.Name) for $($_.Value)" 2165 | New-Alias @_ 2166 | } 2167 | elseif ( $alias.ModuleName -eq 'UnitySetup' ) { 2168 | Write-Verbose "Setting alias $($_.Name) to $($_.Value)" 2169 | Set-Alias @_ 2170 | } 2171 | else { 2172 | Write-Warning "Alias $($_.Name) already configured by $($alias.Source)" 2173 | } 2174 | } 2175 | 2176 | function Import-TOMLFile { 2177 | param( 2178 | [string[]]$TomlFilePaths = @(), 2179 | [switch]$Force 2180 | ) 2181 | 2182 | $tomlFileObjects = @(); 2183 | 2184 | foreach ($tomlFile in $TomlFilePaths) { 2185 | if (-not (Test-Path $tomlFile)) { 2186 | if ($Force) { 2187 | Write-Verbose "$tomlFile doesn't exist, creating $tomlFile" 2188 | New-Item -Path $tomlFile -Force 2189 | } else { 2190 | Write-Error "Toml file not found at $TomlFilePaths" 2191 | } 2192 | } 2193 | 2194 | $tomlFileContent = Get-Content $tomlFile -Raw 2195 | $tomlFileObject = [PSCustomObject]@{ 2196 | tomlFilePath = $tomlFile 2197 | tomlFileContent = $tomlFileContent 2198 | } 2199 | $tomlFileObjects += $tomlFileObject 2200 | } 2201 | 2202 | return $tomlFileObjects 2203 | } 2204 | 2205 | function New-PAT { 2206 | [CmdletBinding(SupportsShouldProcess=$true)] 2207 | [OutputType([string])] 2208 | param ( 2209 | [string]$PATName, 2210 | [string]$OrgName, 2211 | [string]$Scopes, 2212 | [int]$ExpireDays, 2213 | [string]$AzAPIVersion, 2214 | [guid]$AzureSubscription 2215 | ) 2216 | 2217 | 2218 | $expireDate = (Get-Date).AddDays($ExpireDays).ToString('yyyy-MM-ddTHH:mm:ss.fffZ') 2219 | $createPAT = 'y' 2220 | 2221 | if (-not $env:TF_BUILD) { 2222 | $answer = Read-Host "A Personal Access Token (PAT) will be created for you with the following details 2223 | Name: $PATName 2224 | Organization: $OrgName 2225 | Expiration: $expireDate 2226 | Would you like to continue? (Default: $($createPAT))" 2227 | if (-not [string]::IsNullOrEmpty($answer)) { 2228 | $createPAT = $answer 2229 | } 2230 | } 2231 | 2232 | if (($createPAT -like 'y') -or ($createPAT -like 'yes')) { 2233 | } 2234 | else { 2235 | return $null 2236 | } 2237 | 2238 | if (-not $env:TF_BUILD) { 2239 | $azaccount = $(Get-AzContext).Account 2240 | 2241 | if ([string]::IsNullOrEmpty($azaccount) -or (-not $azaccount.Id.Contains("@microsoft.com"))) { 2242 | Write-Verbose "Connecting to Azure, please login if prompted" 2243 | $connectArgs = @{} 2244 | if ($PSBoundParameters.ContainsKey('AzureSubscription')) { 2245 | $connectArgs.Subscription = $AzureSubscription 2246 | } 2247 | Connect-AzAccount @connectArgs | Out-Null 2248 | } 2249 | 2250 | $AZTokenRequest = Get-AzAccessToken -AsSecureString -ResourceType Arm 2251 | $AZToken = [System.Net.NetworkCredential]::new($null, $AZTokenRequest.Token).Password 2252 | 2253 | $headers = @{ Authorization = "Bearer $($AZToken)" } 2254 | } 2255 | else { 2256 | $headers = @{ Authorization = "Bearer $env:SYSTEM_ACCESSTOKEN" } 2257 | } 2258 | 2259 | $RequestBody = @" 2260 | { 2261 | "allOrgs":"false", 2262 | "displayName":"$($PatName)", 2263 | "scope":"$($Scopes)", 2264 | "validTo":"$($expireDate)" 2265 | } 2266 | "@ 2267 | $Url = "https://vssps.dev.azure.com/$($OrgName)/_apis/tokens/pats?api-version=$AzAPIVersion" 2268 | 2269 | if ($PSCmdlet.ShouldProcess("Creating PAT", "PAT Name: $PATName, Organization: $OrgName")) { 2270 | $responseData = (Invoke-WebRequest -Uri $Url -Body $RequestBody -Method Post -Headers $headers -UseBasicParsing -ContentType "application/json").Content | ConvertFrom-Json 2271 | 2272 | $UserPAT = "$($responseData.patToken.token.trim())" 2273 | 2274 | if (Confirm-PAT -Org "$($OrgName)" -Project "$($ProjectName)" -FeedID "$($FeedName)" -RawPAT "$($UserPAT)") { 2275 | return [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes(":" + $UserPAT)) 2276 | } 2277 | else { 2278 | Write-Verbose "Unable to validate PAT, please try again" 2279 | return $null 2280 | } 2281 | } 2282 | } 2283 | 2284 | function Confirm-PAT($Org, $Project, $FeedID, $RawPAT) { 2285 | $user = 'any' 2286 | $pass = $RawPAT 2287 | $pair = "$($user):$($pass)" 2288 | $encodedCreds = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($pair)) 2289 | $basicAuthValue = "Basic $encodedCreds" 2290 | $headers = @{ 2291 | Authorization = $basicAuthValue 2292 | } 2293 | 2294 | $URI = "https://feeds.dev.azure.com/$($Org)" 2295 | if (-not [string]::IsNullOrEmpty($Project)) { 2296 | $URI += "/$($Project)" 2297 | } 2298 | $URI += "/_apis/packaging/feeds/$($FeedID)?api-version=$AzAPIVersion" 2299 | 2300 | Write-Verbose "Attempting to validate PAT for '$($Org)' in feed: '$FeedID'" 2301 | try { 2302 | $req = Invoke-WebRequest -uri $URI -Method 'GET' -Headers $headers -ErrorVariable $WebError -UseBasicParsing -ErrorAction SilentlyContinue 2303 | $HTTP_Status = [int]$req.StatusCode 2304 | } 2305 | catch { 2306 | $HTTP_Status = [int]$_.Exception.Response.StatusCode 2307 | $HTTP_ErrorMessage = $_ 2308 | } 2309 | 2310 | If ($HTTP_Status -eq 200) { 2311 | Write-Verbose "PAT is valid for $Org!" 2312 | $result = $true 2313 | } 2314 | else { 2315 | Write-Warning "Unable to validate PAT for $($Org). Error: $HTTP_ErrorMessage" 2316 | $result = $false 2317 | } 2318 | if ($null -eq $HTTP_Response) { } 2319 | else { $HTTP_Response.Close() } 2320 | 2321 | return $result 2322 | } 2323 | 2324 | function Read-PATFromUser($OrgName) { 2325 | Write-Verbose "You need to create or supply a PAT for $($OrgName)." 2326 | 2327 | Write-Verbose "Please navigate to:" 2328 | Write-Verbose "https://dev.azure.com/$($OrgName)/_usersSettings/tokens" -ForegroundColor Green 2329 | Write-Verbose "to create a PAT with at least 'Package Read' (check your documentation for other scopes)" 2330 | Write-Verbose "" 2331 | 2332 | $launchBrowserForPATs = 'y' 2333 | $launchBrowserForPATs = Read-Host "Launch browser to 'https://dev.azure.com/$($OrgName)/_usersSettings/tokens'? (Default: $($launchBrowserForPATs))" 2334 | if (($launchBrowserForPATs -like 'y') -or ($launchBrowserForPATs -like 'yes') -or [string]::IsNullOrEmpty($launchBrowserForPATs)) { 2335 | Start-Process "https://dev.azure.com/$($OrgName)/_usersSettings/tokens" 2336 | } 2337 | 2338 | $goodPAT = $false 2339 | while (-not $goodPAT) { 2340 | $UserPAT = Read-Host -Prompt "Please enter your PAT for $($OrgName)" 2341 | if (Confirm-PAT -Org "$($OrgName)" -Project "$($ProjectName)" -FeedID "$($FeedName)" -RawPAT "$($UserPAT.trim())") { 2342 | return [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes(":" + $UserPAT.trim())) 2343 | $goodPAT = $true 2344 | } 2345 | else { 2346 | Write-Error "Unable to validate PAT, please try again" 2347 | } 2348 | } 2349 | } 2350 | 2351 | function Get-RegExForConfig($Org, $Project, $Feed, $PAT) { 2352 | $regexresult = "[`r`n]*\[npmAuth\.""https:\/\/pkgs.dev.azure.com\/$($Org)\/" 2353 | if (-not [string]::IsNullOrEmpty($Project)) { 2354 | $regexresult += "$($Project)\/" 2355 | } 2356 | $regexresult += "_packaging\/$($Feed)\/npm\/registry""\][\n\r\s]*_auth ?= ?""$($PAT)""[\n\r\s]*(?:alwaysAuth[\n\r\s]*=[\n\r\s]*true)[\n\r\s]?" 2357 | return $regexresult 2358 | } 2359 | 2360 | function Update-PackageAuthConfig { 2361 | [CmdletBinding()] 2362 | param( 2363 | [string[]]$ScopedRegistryURLs, 2364 | [PSCustomObject[]]$TomlFileObjects, 2365 | [switch]$AutoClean, 2366 | [switch]$VerifyOnly, 2367 | [switch]$ManualPAT, 2368 | [string]$AzAPIVersion, 2369 | [uint]$PATLifetime, 2370 | [string]$DefaultScope, 2371 | [string]$ScopedURLRegEx, 2372 | [string]$UPMRegEx, 2373 | [guid]$AzureSubscription 2374 | ) 2375 | 2376 | $Results = @() 2377 | $UPMConfigs = @() 2378 | 2379 | foreach ($scopedRegistryURL in $ScopedRegistryURLs) { 2380 | Write-Verbose "Resolving $scopedRegistryURL" 2381 | if (-not $url -like 'https://pkgs.dev.azure.com/*') { 2382 | Write-Warning "Scoped registry is not does not match a pkgs.dev.azure.com, automatic PAT generation is likely to fail." 2383 | } 2384 | 2385 | $CurrentRegistry = [Regex]::Match($scopedRegistryURL, $ScopedURLRegEx) 2386 | 2387 | $OrgURL = "$($CurrentRegistry.Groups["OrgURL"])" 2388 | $OrgName = "$($CurrentRegistry.Groups["Org"])" 2389 | $ProjectName = "$($CurrentRegistry.Groups["Project"])" 2390 | $FeedName = "$($CurrentRegistry.Groups["Feed"])" 2391 | 2392 | $foundCount = 0 2393 | 2394 | foreach ($tomlObject in $TomlFileObjects) { 2395 | $tomlFileContent = $tomlObject.tomlFileContent 2396 | $tomlFile = $tomlObject.tomlFilePath 2397 | if (-not [string]::IsNullOrWhiteSpace($tomlFileContent)) { 2398 | [string[]]$FullURLs = @() 2399 | 2400 | foreach ($org in [Regex]::Matches($tomlFileContent, $UPMRegEx)) { 2401 | $FullURL = $org.Groups["FullURL"] 2402 | if ($FullURL -in $FullURLs) { 2403 | Write-Error "Config file $tomlFile contains duplicate entry for $FullURL, will cause error on reading file." 2404 | 2405 | $RemoveBadPAT = 'y' 2406 | if (-not $AutoClean) { 2407 | $RemoveBadPAT = Read-Host "Remove all entries for $($org.Groups["Org"]) $($org.Groups["Project"]) $($org.Groups["Feed"])? (Default: $($RemoveBadPAT))" 2408 | } 2409 | if (($RemoveBadPAT -like 'y') -or ($RemoveBadPAT -like 'yes') -or [string]::IsNullOrEmpty($RemoveBadPAT)) { 2410 | Write-Verbose "Removing all entries for $($org.Groups["Org"]) $($org.Groups["Project"]) $($org.Groups["Feed"])" 2411 | $replaceFilter = (Get-RegExForConfig -Org $org.Groups["Org"] -Project $org.Groups["Project"] -Feed $org.Groups["Feed"] -PAT "$($org.Groups["Token"])") 2412 | $tomlFileContent = $tomlFileContent -replace $replaceFilter, '' 2413 | Set-Content -Path $tomlFile $tomlFileContent 2414 | } 2415 | continue 2416 | } 2417 | $FullURLs += $FullURL 2418 | } 2419 | 2420 | foreach ($org in [Regex]::Matches($tomlFileContent, $UPMRegEx)) { 2421 | if ("$($org.Groups["FullURL"])" -like $scopedRegistryURL) { 2422 | try { 2423 | $reversedPAT = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String("$($org.Groups["Token"])")).trim(':') 2424 | } 2425 | catch { 2426 | Write-Error "Auth appears malformed, unable to convert from base64" 2427 | $RemoveBadPAT = 'y' 2428 | if (-not $AutoClean) { 2429 | $RemoveBadPAT = Read-Host "Unable to validate a cached PAT, it could be expired or otherwise invalid. Remove expired/invalid auth for $OrgName in feed $FeedName? (Default: $($RemoveBadPAT))" 2430 | } 2431 | if (($RemoveBadPAT -like 'y') -or ($RemoveBadPAT -like 'yes') -or [string]::IsNullOrEmpty($RemoveBadPAT)) { 2432 | $replaceFilter = "$(Get-RegExForConfig -Org "$($OrgName)" -Project "$($ProjectName)" -Feed "$($FeedName)" -RawPAT "$($org.Groups["Token"])")" 2433 | $tomlFileContent = $tomlFileContent -replace $replaceFilter, '' 2434 | Set-Content -Path $tomlFile $tomlFileContent 2435 | } 2436 | continue 2437 | } 2438 | 2439 | if (Confirm-PAT -Org "$($OrgName)" -Project "$($ProjectName)" -FeedID "$($FeedName)" -RawPAT $reversedPAT) { 2440 | Write-Verbose "Found: $tomlFile has valid auth for $scopedRegistryURL" 2441 | $AuthState = "Present and valid" 2442 | $foundCount++ 2443 | } 2444 | else { 2445 | $AuthState = "Invalid, failed validation" 2446 | 2447 | if ($VerifyOnly) { 2448 | throw "Invalid PAT found in Verify Mode" 2449 | } 2450 | $RemoveBadPAT = 'y' 2451 | if (-not $AutoClean) { 2452 | $RemoveBadPAT = Read-Host "Unable to validate a cached PAT, it could be expired or otherwise invalid. Remove expired/invalid auth for $OrgName in feed $FeedName? (Default: $($RemoveBadPAT))" 2453 | } 2454 | if (($RemoveBadPAT -like 'y') -or ($RemoveBadPAT -like 'yes') -or [string]::IsNullOrEmpty($RemoveBadPAT)) { 2455 | $replaceFilter = "$(Get-RegExForConfig -Org "$($OrgName)" -Project "$($ProjectName)" -Feed "$($FeedName)" -PAT "$($org.Groups["Token"])")" 2456 | $tomlFileContent = $tomlFileContent -replace $replaceFilter, '' 2457 | Set-Content -Path $tomlFile $tomlFileContent 2458 | } 2459 | } 2460 | } 2461 | } 2462 | 2463 | if ($foundCount -eq 0) { 2464 | $MatchedOrg = $false 2465 | foreach ($org in [Regex]::Matches($tomlFileContent, $UPMRegEx)) { 2466 | if (($org.Groups["OrgURL"]) -like $OrgURL) { 2467 | $reversedPAT = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String("$($org.Groups["Token"])")).trim(':') 2468 | 2469 | if (Confirm-PAT -Org "$($OrgName)" -Project "$($ProjectName)" -FeedID "$($FeedName)" -RawPAT $reversedPAT) { 2470 | Write-Verbose "Existing auth in the same organization, copying existing auth..." 2471 | Write-Verbose "Auth for '$($org.Groups["OrgURL"])$($org.Groups["ProjectURL"])$($org.Groups["FeedID"])' will be copied for $scopedRegistryURL" 2472 | 2473 | $tomlConfigContent = @( 2474 | "`r`n[npmAuth.""$scopedRegistryURL""]" 2475 | "_auth = ""$($org.Groups["Token"])""" 2476 | "alwaysAuth = true" 2477 | ) -join "`r`n" 2478 | Add-Content -Path $tomlFile -Value $tomlConfigContent 2479 | $MatchedOrg = $true 2480 | $AuthState = "Verified and copied existing auth from same org" 2481 | $foundCount++ 2482 | } 2483 | else { 2484 | Write-Verbose "Existing auth in the same organization found, but it appears to be expired or otherwise invalid" 2485 | } 2486 | } 2487 | if ($MatchedOrg) { 2488 | break 2489 | } 2490 | } 2491 | if (-not $MatchedOrg) { 2492 | $AuthState = "Not found!" 2493 | Write-Verbose "No suitable auth inside $tomlFile for $scopedRegistryURL" 2494 | } 2495 | } 2496 | } 2497 | } 2498 | 2499 | $ScopedPAT = '' 2500 | if ($foundCount -eq 0) { 2501 | if ($VerifyOnly) { 2502 | throw "No PAT found in Verify Mode" 2503 | } 2504 | 2505 | if($env:TF_BUILD) { 2506 | if(Confirm-PAT "$($OrgName)" "$($ProjectName)" "$($FeedName)" $env:SYSTEM_ACCESSTOKEN) { 2507 | Write-Verbose "System access token found" 2508 | $ScopedPAT = $env:SYSTEM_ACCESSTOKEN 2509 | $AuthState = "Applied from system PAT" 2510 | } else { 2511 | Write-Error "System access token found, but it was invalid for this org" 2512 | $AuthState = "PAT is invalid" 2513 | } 2514 | } 2515 | 2516 | if (![string]::IsNullOrEmpty($ScopedPAT)) { 2517 | $convertedScopedPAT = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes(":" + $ScopedPAT.trim())) 2518 | } 2519 | else { 2520 | Write-Verbose "Missing authentication for $scopedRegistryURL" 2521 | Write-Verbose "" 2522 | if ($ManualPAT) { 2523 | $newPAT = Read-PATFromUser($OrgName) 2524 | } 2525 | else { 2526 | $newPatArgs = @{ 2527 | 'PATName' = "$($OrgName)_Package-Read (Automated)" 2528 | 'OrgName' = "$($OrgName)" 2529 | 'Scopes' = "$($DefaultScope)" 2530 | 'ExpireDays' = $PATLifetime 2531 | 'AzAPIVersion' = $AzAPIVersion 2532 | } 2533 | 2534 | if($PSBoundParameters.ContainsKey('AzureSubscription')){ 2535 | $newPatArgs.AzureSubscription = $AzureSubscription 2536 | } 2537 | 2538 | $newPAT = $(New-PAT @newPatArgs) 2539 | } 2540 | if (-not [string]::IsNullOrEmpty($newPAT)) { 2541 | $convertedScopedPAT = $newPAT 2542 | $AuthState = "Applied from user" 2543 | } 2544 | else { 2545 | $AuthState = "Failed to validate PAT from user" 2546 | } 2547 | } 2548 | 2549 | if (-not $convertedScopedPAT) { 2550 | Write-Error "Auth not found for $scopedRegistryURL and no valid PAT to add" 2551 | $AuthState = "Missing" 2552 | continue 2553 | } 2554 | 2555 | Write-Verbose "Auth not found for $scopedRegistryURL. Adding using supplied PAT..." 2556 | 2557 | $UPMConfigs += [PSCustomObject]@{ 2558 | ScopedURL = $scopedRegistryURL 2559 | Auth = $convertedScopedPAT 2560 | } 2561 | } 2562 | $Results += [PSCustomObject]@{ 2563 | ScopedURL = $scopedRegistryURL 2564 | AuthState = $AuthState 2565 | } 2566 | } 2567 | 2568 | return $UPMConfigs 2569 | } 2570 | 2571 | function Export-UPMConfig { 2572 | [CmdletBinding()] 2573 | param( 2574 | [PSCustomObject[]]$UPMConfig, 2575 | [string[]]$TomlFilePaths 2576 | ) 2577 | 2578 | foreach ($config in $UPMConfig) { 2579 | if (![string]::IsNullOrEmpty($config.ScopedURL) -and ![string]::IsNullOrEmpty($config.Auth)) { 2580 | $scopedRegistryURL = $config.ScopedURL 2581 | $convertedScopedPAT = $config.Auth 2582 | 2583 | $tomlConfigContent = @( 2584 | "`r`n[npmAuth.""$scopedRegistryURL""]" 2585 | "_auth = ""$convertedScopedPAT""" 2586 | "alwaysAuth = true" 2587 | ) -join "`r`n" 2588 | 2589 | foreach ($filePath in $TomlFilePaths) { 2590 | Add-Content -Path $filePath -Value $tomlConfigContent 2591 | } 2592 | } 2593 | } 2594 | } 2595 | 2596 | function Get-ScopedRegistry { 2597 | param( 2598 | [PSCustomObject[]]$ProjectManifests 2599 | ) 2600 | 2601 | $scopedRegistrySet = [System.Collections.Generic.HashSet[string]]::new() 2602 | 2603 | foreach ($manifest in $ProjectManifests) { 2604 | foreach ($scopedRegistry in $manifest.scopedRegistries) { 2605 | $url = $scopedRegistry.url -replace '/$', '' 2606 | $scopedRegistrySet.Add($url) | Out-Null 2607 | } 2608 | } 2609 | 2610 | return $scopedRegistrySet 2611 | } 2612 | 2613 | <# 2614 | .SYNOPSIS 2615 | Imports Unity project manifest files from the specified path(s). 2616 | 2617 | .DESCRIPTION 2618 | This function searches for Unity project manifest files (manifest.json) within a specified directory path and its subdirectories up to a specified depth. 2619 | It can also directly import a manifest file if a direct file path is provided. Returns an array of the imported manifest data. 2620 | 2621 | .PARAMETER ProjectManifestPath 2622 | The path to a specific Unity project manifest file (manifest.json) or the root directory path where the search for manifest files should begin. 2623 | 2624 | .PARAMETER SearchPath 2625 | The root directory path where the search for manifest files should begin. If not provided, the function will use the ProjectManifestPath. 2626 | 2627 | .PARAMETER SearchDepth 2628 | The depth to which the search should recurse within the directory. The default value is 3. 2629 | 2630 | .EXAMPLE 2631 | Import-UnityProjectManifest -ProjectManifestPath "C:\Projects\UnityProject\manifest.json" 2632 | This example imports the manifest file directly from the specified path. 2633 | 2634 | .EXAMPLE 2635 | Import-UnityProjectManifest -SearchPath "C:\Projects" -SearchDepth 2 2636 | This example searches for manifest.json files within the "C:\Projects" directory and its subdirectories up to a depth of 2 and imports them. 2637 | 2638 | .EXAMPLE 2639 | Import-UnityProjectManifest -ProjectManifestPath "C:\Projects\UnityProject" 2640 | This example searches for manifest.json files within the specified Unity project directory and imports them. 2641 | #> 2642 | function Import-UnityProjectManifest { 2643 | [CmdletBinding()] 2644 | param( 2645 | [ValidateScript({if(-not [string]::IsNullOrEmpty($_)) { Test-Path $_ -PathType Leaf }}, ErrorMessage = "`"{0}`" is not a valid file")] 2646 | [ValidateNotNullOrEmpty()] 2647 | [Parameter(Mandatory=$true, ParameterSetName = "ProjectManifest")] 2648 | [Parameter(Mandatory=$true, ParameterSetName = "SearchPathAndProjectManifest")] 2649 | [string]$ProjectManifestPath, 2650 | 2651 | [ValidateNotNullOrEmpty()] 2652 | [Parameter(Mandatory=$true, ParameterSetName = "SearchPath")] 2653 | [Parameter(Mandatory=$true, ParameterSetName = "SearchPathAndProjectManifest")] 2654 | [string]$SearchPath, 2655 | 2656 | [Parameter(Mandatory=$false, ParameterSetName = "SearchPath")] 2657 | [Parameter(Mandatory=$false, ParameterSetName = "SearchPathAndProjectManifest")] 2658 | [uint]$SearchDepth = 3 2659 | ) 2660 | 2661 | $projectManifestPaths = @() 2662 | 2663 | if ($PSBoundParameters.ContainsKey('SearchPath')) { 2664 | Write-Verbose "Search path ($SearchPath) provided, will attempt search within depth $SearchDepth" 2665 | $FoundPaths = @(Get-ChildItem -Path $SearchPath -Include manifest.json -File -Recurse -Depth $SearchDepth -ErrorAction SilentlyContinue) 2666 | if ($FoundPaths.Count -eq 0) { 2667 | Write-Verbose "No manifest.json files found in directory ($SearchPath) within depth $SearchDepth" 2668 | } 2669 | foreach ($file in $FoundPaths) { 2670 | $projectManifestPaths += $file 2671 | Write-Verbose "Found manifest.json file at ($file)" 2672 | } 2673 | } 2674 | 2675 | if ($PSBoundParameters.ContainsKey('ProjectManifestPath')) { 2676 | Write-Verbose "Path provided is a file ($ProjectManifestPath)" 2677 | $projectManifestPaths += $ProjectManifestPath 2678 | } 2679 | 2680 | $manifests = @() 2681 | foreach ($manifestPath in $projectManifestPaths) { 2682 | try { 2683 | $manifest = Get-Content -Path $manifestPath | ConvertFrom-Json 2684 | $manifests += $manifest 2685 | Write-Verbose "Successfully imported manifest from ($manifestPath)" 2686 | } 2687 | catch { 2688 | Write-Verbose "Failed to import manifest from ($manifestPath): $_" 2689 | } 2690 | } 2691 | 2692 | return $manifests 2693 | } 2694 | 2695 | <# 2696 | .Synopsis 2697 | Ensures that the user has the appropriate auth tokens to fetch Unity packages in their .toml file. 2698 | 2699 | For more information on Unity Package Manager config, please visit https://docs.unity3d.com/Manual/upm-config.html 2700 | .DESCRIPTION 2701 | Looks at the Unity Project Manifest and finds the scoped registries used for fetching NPM packages. 2702 | 2703 | For each of the scoped registries found within the project manifest(s) or SOT.json file, the cmdlet will verify that 2704 | there is a valid auth token for each scoped registry URL. If none were found, it will try to fetch a new auth token 2705 | and save it to the .toml file. 2706 | 2707 | Additional arguments are available to automatically clean expired tokens, allow the user to manually propulate the token, 2708 | scan a deeper folder tree for manifests, or simply validate your existing PATs. 2709 | .PARAMETER ProjectManifestPath 2710 | A path to a project manifest, or a path to a root directory under which Unity project manifests can be found. 2711 | .PARAMETER AutoClean 2712 | Automatically remove PATs that can't be validated 2713 | .PARAMETER ManualPAT 2714 | Do not use Azure APIs to automatically create the PAT, user will manually enter it 2715 | .PARAMETER SearchDepth 2716 | How deep to search for manifest files 2717 | .PARAMETER VerifyOnly 2718 | Runs in validation only mode, returns 0 if all registries are valid, otherwise returns 1 2719 | .PARAMETER PATLifetime 2720 | How many days the created PAT is valid 2721 | .PARAMETER AzureSubscription 2722 | The default subscription ID to use when logging into Azure 2723 | .EXAMPLE 2724 | Update-UnityPackageManagerConfig -ProjectManifestPath '/User/myusername/MyUnityProjectRoot' 2725 | .EXAMPLE 2726 | Update-UnityPackageManagerConfig -ProjectManifestPath '/User/myusername/MyUnityProjectRoot/manifest.json' 2727 | .EXAMPLE 2728 | Update-UnityPackageManagerConfig -AutoClean True 2729 | .EXAMPLE 2730 | Update-UnityPackageManagerConfig -ProjectManifestPath '/User/myusername/MyUnityProjectRoot' -SearchDepth 7 -VerifyOnly True 2731 | #> 2732 | # Disable PSUseShouldProcessForStateChangingFunctions 2733 | function Update-UnityPackageManagerConfig { 2734 | [CmdletBinding(SupportsShouldProcess=$true)] 2735 | param( 2736 | [ValidateScript({if(-not [string]::IsNullOrEmpty($_)) { Test-Path $_ -PathType Leaf }}, ErrorMessage = "`"{0}`" is not a valid file")] 2737 | [ValidateNotNullOrEmpty()] 2738 | [Parameter(Mandatory=$true, ParameterSetName = "ProjectManifest")] 2739 | [Parameter(Mandatory=$true, ParameterSetName = "SearchPathAndProjectManifest")] 2740 | [string]$ProjectManifestPath, 2741 | 2742 | [ValidateNotNullOrEmpty()] 2743 | [Parameter(Mandatory=$true, ParameterSetName = "SearchPath")] 2744 | [Parameter(Mandatory=$true, ParameterSetName = "SearchPathAndProjectManifest")] 2745 | [string]$SearchPath, 2746 | 2747 | [Parameter(Mandatory=$false, ParameterSetName = "SearchPath")] 2748 | [Parameter(Mandatory=$false, ParameterSetName = "SearchPathAndProjectManifest")] 2749 | [uint]$SearchDepth = 3, 2750 | 2751 | [switch]$AutoClean, 2752 | [switch]$ManualPAT, 2753 | [switch]$VerifyOnly, 2754 | [ValidateScript({$_ -gt 0}, ErrorMessage = "PATLifetime must be greater than zero")] 2755 | [uint]$PATLifetime = 7, 2756 | [ValidateScript({$_ -ne [guid]::Empty}, ErrorMessage = "Cannot be empty guid.")] 2757 | [guid]$AzureSubscription 2758 | ) 2759 | 2760 | $scopedURLRegEx = "(?(?https:\/\/pkgs.dev.azure.com\/(?[a-zA-Z0-9]*))\/?(?[a-zA-Z0-9]*)?\/_packaging\/(?[a-zA-Z0-9\-_\.%\(\)!]*)?\/npm\/registry\/?)" 2761 | $upmRegEx = "\[npmAuth\.""(?(?https:\/\/pkgs.dev.azure.com\/(?[a-zA-Z0-9]*))\/?(?[a-zA-Z0-9]*)?\/_packaging\/(?[a-zA-Z0-9\-_\.%\(\)!]*)?\/npm\/registry\/?)""\][\n\r\s]*_auth ?= ?""(?[a-zA-Z0-9=]*)""[\n\r\s]*(?:alwaysAuth[\n\r\s]*=[\n\r\s]*true)[\n\r\s]*" 2762 | $azAPIVersion = '7.1-preview.1' 2763 | $defaultScope = 'vso.packaging' 2764 | 2765 | $nonInteractive = [Environment]::GetCommandLineArgs() | Where-Object { $_ -like '-NonI*' } 2766 | if (-not [Environment]::UserInteractive -or $nonInteractive) { 2767 | $AutoClean = $true 2768 | } 2769 | 2770 | $tomlFilePaths = @() 2771 | if ($IsMacOS -or $IsLinux) { 2772 | $tomlFilePaths += [io.path]::combine($env:HOME, ".upmconfig.toml") 2773 | } 2774 | else { 2775 | $tomlFilePaths += [io.path]::combine($env:USERPROFILE, ".upmconfig.toml") 2776 | } 2777 | 2778 | $importUnityProjectManifestParams = @{} 2779 | 2780 | if($PSBoundParameters.ContainsKey('ProjectManifestPath')) { 2781 | $importUnityProjectManifestParams.ProjectManifestPath = $ProjectManifestPath 2782 | } 2783 | 2784 | if($PSBoundParameters.ContainsKey('SearchPath')) { 2785 | $importUnityProjectManifestParams.SearchPath = $SearchPath 2786 | } 2787 | 2788 | if($PSBoundParameters.ContainsKey('SearchDepth')) { 2789 | $importUnityProjectManifestParams.SearchDepth = $SearchDepth 2790 | } 2791 | 2792 | $projectManifests = Import-UnityProjectManifest @importUnityProjectManifestParams 2793 | $scopedRegistryURLs = Get-ScopedRegistry -ProjectManifests $projectManifests 2794 | $tomlFileObjects = Import-TOMLFile -TomlFilePaths $tomlFilePaths -Force 2795 | 2796 | if ($PSCmdlet.ShouldProcess("Synchronizing UPM configuration")) { 2797 | $updatePackageAuthParams = @{ 2798 | 'ScopedRegistryURLs' = $scopedRegistryURLs 2799 | 'TomlfileObjects' = $tomlFileObjects 2800 | 'PATLifetime' = $PATLifetime 2801 | 'DefaultScope' = $defaultScope 2802 | 'AzAPIVersion' = $azAPIVersion 2803 | 'ScopedURLRegEx' = $scopedURLRegEx 2804 | 'UPMRegEx' = $upmRegEx 2805 | } 2806 | 2807 | if($AutoClean) { $updatePackageAuthParams.AutoClean = $true } 2808 | if($VerifyOnly) { $updatePackageAuthParams.VerifyOnly = $true } 2809 | if($ManualPAT) { $updatePackageAuthParams.ManualPAT = $true } 2810 | 2811 | if($PSBoundParameters.ContainsKey('AzureSubscription')){ 2812 | $updatePackageAuthParams.AzureSubscription = $AzureSubscription 2813 | } 2814 | 2815 | $upmConfigs = Update-PackageAuthConfig @updatePackageAuthParams 2816 | 2817 | if ($PSCmdlet.ShouldProcess("Exporting UPM configuration")) { 2818 | Export-UPMConfig -UPMConfig $upmConfigs -tomlFilePaths $tomlFilePaths 2819 | } 2820 | 2821 | if ($upmConfigs) { 2822 | Write-Verbose "Summary" 2823 | $upmConfigs = $upmConfigs | ForEach-Object { 2824 | [PSCustomObject]@{ 2825 | ScopedURL = $_.ScopedURL 2826 | Succeeded = -not [string]::IsNullOrEmpty($_.Auth) 2827 | } 2828 | } | Format-Table -AutoSize | Out-String 2829 | 2830 | Write-Verbose $upmConfigs 2831 | } else { 2832 | Write-Verbose "No changes were made" 2833 | } 2834 | } 2835 | 2836 | if ($VerifyOnly) { 2837 | Write-Verbose "Verify Mode complete" 2838 | } 2839 | } 2840 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '{build}' 2 | skip_tags: true 3 | pull_requests: 4 | do_not_increment_build_number: true 5 | install: 6 | - ps: >- 7 | $ErrorActionPreference = 'Stop' 8 | 9 | Install-PackageProvider -Name NuGet -Force 10 | 11 | Remove-Module 'PowerShellGet' -Force -ErrorAction SilentlyContinue -Verbose 12 | 13 | Install-Module 'PowerShellGet' -Scope CurrentUser -Force -AllowClobber -Verbose 14 | 15 | Install-Module 'powershell-yaml' -Scope CurrentUser -Force -AllowClobber -Verbose 16 | 17 | Install-Module 'Az.Accounts' -RequiredVersion 2.15.1 -Scope CurrentUser -Force -AllowClobber -Verbose 18 | build_script: 19 | - ps: .\build.ps1 -Revision "$env:APPVEYOR_BUILD_NUMBER" -Suffix "$env:APPVEYOR_REPO_BRANCH" 20 | deploy_script: 21 | - ps: >- 22 | $publish = Start-Process 'powershell' -Wait -PassThru -RedirectStandardError .\deploy_error.log -RedirectStandardOutput .\deploy_out.log { Publish-Module -Path .\UnitySetup -NugetAPIKey $env:NugetAPIKey -Verbose } 23 | 24 | Get-Content .\deploy_output.log -ErrorAction SilentlyContinue | Write-Host 25 | 26 | Get-Content .\deploy_error.log -ErrorAction SilentlyContinue | Write-Host -ForegroundColor Red 27 | 28 | if( $publish.ExitCode -ne 0 ) { Write-Error "Publish step failed - see above logs"} 29 | for: 30 | - 31 | branches: 32 | only: 33 | - master 34 | build_script: 35 | - ps: .\build.ps1 -Revision "$env:APPVEYOR_BUILD_NUMBER" 36 | - 37 | branches: 38 | only: 39 | - develop 40 | -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | 4 | param([int]$Revision = 0, [string]$Suffix = '') 5 | Import-Module 'PowerShellGet' -Force 6 | 7 | $ErrorActionPreference = 'Stop' 8 | 9 | $manifest = Test-ModuleManifest .\UnitySetup\UnitySetup.psd1 10 | $versionString = $manifest.Version.ToString() 11 | if ($manifest.PrivateData['PSData']['Prerelease']) { 12 | $versionString += "-$($manifest.PrivateData['PSData']['Prerelease'])" 13 | } 14 | Write-Host "Current Module Version: $versionString" 15 | 16 | $newVersion = New-Object System.Version($manifest.Version.Major, $manifest.Version.Minor, $Revision) 17 | Update-ModuleManifest -ModuleVersion $newVersion -Prerelease $Suffix -Path .\UnitySetup\UnitySetup.psd1 18 | 19 | $manifest = Test-ModuleManifest .\UnitySetup\UnitySetup.psd1 20 | $versionString = $manifest.Version.ToString() 21 | if ($manifest.PrivateData['PSData']['Prerelease']) { 22 | $versionString += "-$($manifest.PrivateData['PSData']['Prerelease'])" 23 | } 24 | Write-Host "New Module Version: $versionString" --------------------------------------------------------------------------------