diff --git a/README.md b/README.md index 86a8ae4..422a355 100644 --- a/README.md +++ b/README.md @@ -55,12 +55,12 @@ Find all the Unity projects recursively: Get-UnityProjectInstance -Recurse # Example output: -# Version Path -# ------- ---- -# 2017.2.0f3 C:\Projects\Project1\OneUnity\ -# 2017.3.0f3 C:\Projects\Project1\TwoUnity\ -# 2017.1.1p1 C:\Projects\Project2\ -# 2017.1.2f1 C:\Projects\Project3\App.Unity\ +# Version Path ProductName +# ------- ---- ----------- +# 2017.2.0f3 C:\Projects\Project1\OneUnity\ Contoso +# 2017.3.0f3 C:\Projects\Project1\TwoUnity\ Northwind +# 2017.1.1p1 C:\Projects\Project2\ My Cool App +# 2017.1.2f1 C:\Projects\Project3\App.Unity\ TemplateProject ``` Launch the right Unity editor for a project: ```powershell @@ -73,6 +73,10 @@ Launch many projects at the same time: ```powershell Get-UnityProjectInstance -Recurse | Start-UnityEditor ``` +Invoke methods with arbitrary arguments: +```powershell +Start-UnityEditor -ExecuteMethod Build.Invoke -BatchMode -Quit -LogFile .\build.log -Wait -AdditionalArguments "-BuildArg1 -BuildArg2" +``` Find the installers for a particular version: ```powershell Find-UnitySetupInstaller -Version '2017.3.0f3' | Format-Table diff --git a/UnitySetup/UnitySetup.psd1 b/UnitySetup/UnitySetup.psd1 index 2bc02e1..d823d65 100644 --- a/UnitySetup/UnitySetup.psd1 +++ b/UnitySetup/UnitySetup.psd1 @@ -14,7 +14,7 @@ RootModule = 'UnitySetup' # Version number of this module. - ModuleVersion = '5.0' + ModuleVersion = '5.1' # Supported PSEditions # CompatiblePSEditions = @() @@ -76,11 +76,15 @@ FunctionsToExport = @( 'Find-UnitySetupInstaller', + 'Select-UnitySetupInstaller', + 'Test-UnitySetupInstance', 'Get-UnityProjectInstance', 'Get-UnitySetupInstance', + 'Request-UnitySetupInstaller', 'Install-UnitySetupInstance', 'Select-UnitySetupInstance', 'Uninstall-UnitySetupInstance', + 'Get-UnityEditor', 'Start-UnityEditor', 'ConvertTo-UnitySetupComponent', 'Get-UnityLicense' @@ -97,6 +101,7 @@ 'gusi', 'gupi', 'susi', + 'gue', 'sue' ) diff --git a/UnitySetup/UnitySetup.psm1 b/UnitySetup/UnitySetup.psm1 index beb346d..eaf64bc 100644 --- a/UnitySetup/UnitySetup.psm1 +++ b/UnitySetup/UnitySetup.psm1 @@ -19,7 +19,9 @@ enum UnitySetupComponent { Facebook = (1 -shl 11) Vuforia = (1 -shl 12) WebGL = (1 -shl 13) - All = (1 -shl 14) - 1 + Mac_IL2CPP = (1 -shl 14) + Lumin = (1 -shl 15) + All = (1 -shl 16) - 1 } [Flags()] @@ -29,6 +31,11 @@ enum OperatingSystem { Mac } +class UnitySetupResource { + [UnitySetupComponent] $ComponentType + [string] $Path +} + class UnitySetupInstaller { [UnitySetupComponent] $ComponentType [UnityVersion] $Version @@ -43,24 +50,11 @@ class UnitySetupInstance { [string]$Path UnitySetupInstance([string]$path) { - $currentOS = Get-OperatingSystem - $ivyPath = switch ($currentOS) { - ([OperatingSystem]::Windows) { 'Editor\Data\UnityExtensions\Unity\Networking\ivy.xml' } - ([OperatingSystem]::Linux) { throw "UnitySetupInstance has not been implemented on the Linux platform. Contributions welcomed!"; } - ([OperatingSystem]::Mac) { 'Unity.app/Contents/UnityExtensions/Unity/Networking/ivy.xml' } - } - - $ivyPath = [io.path]::Combine("$path", $ivyPath); - if (!(Test-Path $ivyPath)) { throw "Path is not a Unity setup: $path"} - [xml]$xmlDoc = Get-Content $ivyPath - - if ( !($xmlDoc.'ivy-module'.info.unityVersion)) { - throw "Unity setup ivy is missing version: $ivyPath" - } $this.Path = $path - $this.Version = $xmlDoc.'ivy-module'.info.unityVersion + $this.Version = Get-UnitySetupInstanceVersion -Path $path + if ( -not $this.Version ) { throw "Unable to find version for $path" } $playbackEnginePath = $null $componentTests = switch ($currentOS) { @@ -68,14 +62,14 @@ class UnitySetupInstance { $this.Components = [UnitySetupComponent]::Windows $playbackEnginePath = [io.path]::Combine("$Path", "Editor\Data\PlaybackEngines"); @{ - [UnitySetupComponent]::Documentation = , [io.path]::Combine("$Path", "Editor\Data\Documentation"); + [UnitySetupComponent]::Documentation = , [io.path]::Combine("$Path", "Editor\Data\Documentation"); [UnitySetupComponent]::StandardAssets = , [io.path]::Combine("$Path", "Editor\Standard Assets"); [UnitySetupComponent]::Windows_IL2CPP = , [io.path]::Combine("$playbackEnginePath", "windowsstandalonesupport\Variations\win32_development_il2cpp"); - [UnitySetupComponent]::UWP = [io.path]::Combine("$playbackEnginePath", "MetroSupport\Templates\UWP_.NET_D3D"), - [io.path]::Combine("$playbackEnginePath", "MetroSupport\Templates\UWP_D3D"); - [UnitySetupComponent]::UWP_IL2CPP = , [io.path]::Combine("$playbackEnginePath", "MetroSupport\Templates\UWP_IL2CPP_D3D"); - [UnitySetupComponent]::Linux = , [io.path]::Combine("$playbackEnginePath", "LinuxStandaloneSupport"); - [UnitySetupComponent]::Mac = , [io.path]::Combine("$playbackEnginePath", "MacStandaloneSupport"); + [UnitySetupComponent]::UWP = [io.path]::Combine("$playbackEnginePath", "MetroSupport\Templates\UWP_.NET_D3D"), + [io.path]::Combine("$playbackEnginePath", "MetroSupport\Templates\UWP_D3D"); + [UnitySetupComponent]::UWP_IL2CPP = , [io.path]::Combine("$playbackEnginePath", "MetroSupport\Templates\UWP_IL2CPP_D3D"); + [UnitySetupComponent]::Linux = , [io.path]::Combine("$playbackEnginePath", "LinuxStandaloneSupport"); + [UnitySetupComponent]::Mac = , [io.path]::Combine("$playbackEnginePath", "MacStandaloneSupport"); } } ([OperatingSystem]::Linux) { @@ -87,21 +81,23 @@ class UnitySetupInstance { $this.Components = [UnitySetupComponent]::Mac $playbackEnginePath = [io.path]::Combine("$Path", "PlaybackEngines"); @{ - [UnitySetupComponent]::Documentation = , [io.path]::Combine("$Path", "Documentation"); + [UnitySetupComponent]::Documentation = , [io.path]::Combine("$Path", "Documentation"); [UnitySetupComponent]::StandardAssets = , [io.path]::Combine("$Path", "Standard Assets"); - [UnitySetupComponent]::Windows = , [io.path]::Combine("$playbackEnginePath", "WindowsStandaloneSupport"); - [UnitySetupComponent]::Linux = , [io.path]::Combine("$playbackEnginePath", "LinuxStandaloneSupport"); + [UnitySetupComponent]::Mac_IL2CPP = , [io.path]::Combine("$playbackEnginePath", "MacStandaloneSupport/Variations/macosx64_development_il2cpp"); + [UnitySetupComponent]::Windows = , [io.path]::Combine("$playbackEnginePath", "WindowsStandaloneSupport"); + [UnitySetupComponent]::Linux = , [io.path]::Combine("$playbackEnginePath", "LinuxStandaloneSupport"); } } } # Common playback engines: - $componentTests[[UnitySetupComponent]::Android] = , [io.path]::Combine("$playbackEnginePath", "AndroidPlayer"); - $componentTests[[UnitySetupComponent]::iOS] = , [io.path]::Combine("$playbackEnginePath", "iOSSupport"); - $componentTests[[UnitySetupComponent]::AppleTV] = , [io.path]::Combine("$playbackEnginePath", "AppleTVSupport"); + $componentTests[[UnitySetupComponent]::Lumin] = , [io.path]::Combine("$playbackEnginePath", "LuminSupport"); + $componentTests[[UnitySetupComponent]::Android] = , [io.path]::Combine("$playbackEnginePath", "AndroidPlayer"); + $componentTests[[UnitySetupComponent]::iOS] = , [io.path]::Combine("$playbackEnginePath", "iOSSupport"); + $componentTests[[UnitySetupComponent]::AppleTV] = , [io.path]::Combine("$playbackEnginePath", "AppleTVSupport"); $componentTests[[UnitySetupComponent]::Facebook] = , [io.path]::Combine("$playbackEnginePath", "Facebook"); - $componentTests[[UnitySetupComponent]::Vuforia] = , [io.path]::Combine("$playbackEnginePath", "VuforiaSupport"); - $componentTests[[UnitySetupComponent]::WebGL] = , [io.path]::Combine("$playbackEnginePath", "WebGLSupport"); + $componentTests[[UnitySetupComponent]::Vuforia] = , [io.path]::Combine("$playbackEnginePath", "VuforiaSupport"); + $componentTests[[UnitySetupComponent]::WebGL] = , [io.path]::Combine("$playbackEnginePath", "WebGLSupport"); $componentTests.Keys | ForEach-Object { foreach ( $test in $componentTests[$_] ) { @@ -117,6 +113,7 @@ class UnitySetupInstance { class UnityProjectInstance { [UnityVersion]$Version [string]$Path + [string]$ProductName UnityProjectInstance([string]$path) { $versionFile = [io.path]::Combine($path, "ProjectSettings\ProjectVersion.txt") @@ -125,8 +122,15 @@ class UnityProjectInstance { $fileVersion = (Get-Content $versionFile -Raw | ConvertFrom-Yaml)['m_EditorVersion']; if (!$fileVersion) { throw "Project is missing a version in: $versionFile"} + $projectSettingsFile = [io.path]::Combine($path, "ProjectSettings\ProjectSettings.asset") + if (!(Test-Path $projectSettingsFile)) { throw "Project is missing ProjectSettings.asset"} + + $prodName = ((Get-Content $projectSettingsFile -Raw | ConvertFrom-Yaml)['playerSettings'])['productName'] + if (!$prodName) { throw "ProjectSettings is missing productName"} + $this.Path = $path $this.Version = $fileVersion + $this.ProductName = $prodName } } @@ -147,7 +151,7 @@ class UnityVersion : System.IComparable { UnityVersion([string] $version) { $parts = $version.Split('-') - $parts[0] -match "(\d+)\.(\d+)\.(\d+)([fpb])(\d+)" | Out-Null + $parts[0] -match "(\d+)\.(\d+)\.(\d+)([fpba])(\d+)" | Out-Null if ( $Matches.Count -ne 6 ) { throw "Invalid unity version: $version" } $this.Major = [int]($Matches[1]); $this.Minor = [int]($Matches[2]); @@ -214,7 +218,7 @@ class UnityVersion : System.IComparable { ([OperatingSystem]::Mac) { echo "On Mac" } } .EXAMPLE - if (Get-OperatingSystem == [OperatingSystem]::Linux) { + if (Get-OperatingSystem -eq [OperatingSystem]::Linux) { echo "On Linux" } #> @@ -230,6 +234,57 @@ function Get-OperatingSystem { } } +<# +.Synopsis + Get the Unity Editor application +.PARAMETER Path + Path of a UnitySetupInstance +.EXAMPLE + Get-UnityEditor -Path $unitySetupInstance.Path +#> +function Get-UnityEditor { + [CmdletBinding()] + param( + [ValidateScript( {Test-Path $_ -PathType Container} )] + [Parameter(Mandatory = $false, ValueFromPipeline = $true, Position = 0, ParameterSetName = "Path")] + [string[]]$Path = $PWD, + + [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0, ParameterSetName = "Instance")] + [ValidateNotNull()] + [UnitySetupInstance[]]$Instance + ) + + process { + + if ( $PSCmdlet.ParameterSetName -eq "Instance" ) { + $Path = $Instance.Path + } + + $currentOS = Get-OperatingSystem + foreach ($p in $Path) { + switch ($currentOS) { + ([OperatingSystem]::Windows) { + $editor = Join-Path "$p" 'Editor\Unity.exe' + + if (Test-Path $editor) { + Write-Output (Resolve-Path $editor).Path + } + } + ([OperatingSystem]::Linux) { + throw "Get-UnityEditor has not been implemented on the Linux platform. Contributions welcomed!"; + } + ([OperatingSystem]::Mac) { + $editor = Join-Path "$p" "Unity.app/Contents/MacOS/Unity" + + if (Test-Path $editor) { + Write-Output (Resolve-Path $editor).Path + } + } + } + } + } +} + <# .Synopsis Help to create UnitySetupComponent @@ -275,7 +330,6 @@ function Find-UnitySetupInstaller { $currentOS = Get-OperatingSystem switch ($currentOS) { ([OperatingSystem]::Windows) { - $unitySetupRegEx = "^(.+)\/([a-z0-9]+)\/Windows64EditorInstaller\/UnitySetup64-(\d+)\.(\d+)\.(\d+)([fpb])(\d+).exe$" $targetSupport = "TargetSupportInstaller" $installerExtension = "exe" } @@ -283,12 +337,13 @@ function Find-UnitySetupInstaller { throw "Find-UnitySetupInstaller has not been implemented on the Linux platform. Contributions welcomed!"; } ([OperatingSystem]::Mac) { - $unitySetupRegEx = "^(.+)\/([a-z0-9]+)\/MacEditorInstaller\/Unity-(\d+)\.(\d+)\.(\d+)([fpb])(\d+).pkg$" $targetSupport = "MacEditorTargetInstaller" $installerExtension = "pkg" } } + $unitySetupRegEx = "^(.+)\/([a-z0-9]+)\/(.+)\/(.+)-(\d+)\.(\d+)\.(\d+)([fpba])(\d+).$installerExtension$" + $knownBaseUrls = @( "https://download.unity3d.com/download_unity", "https://netstorage.unity3d.com/unity", @@ -296,19 +351,22 @@ function Find-UnitySetupInstaller { ) $installerTemplates = @{ - [UnitySetupComponent]::UWP = "$targetSupport/UnitySetup-UWP-.NET-Support-for-Editor-$Version.$installerExtension", - "$targetSupport/UnitySetup-Metro-Support-for-Editor-$Version.$installerExtension"; - [UnitySetupComponent]::UWP_IL2CPP = , "$targetSupport/UnitySetup-UWP-IL2CPP-Support-for-Editor-$Version.$installerExtension"; - [UnitySetupComponent]::Android = , "$targetSupport/UnitySetup-Android-Support-for-Editor-$Version.$installerExtension"; - [UnitySetupComponent]::iOS = , "$targetSupport/UnitySetup-iOS-Support-for-Editor-$Version.$installerExtension"; - [UnitySetupComponent]::AppleTV = , "$targetSupport/UnitySetup-AppleTV-Support-for-Editor-$Version.$installerExtension"; - [UnitySetupComponent]::Facebook = , "$targetSupport/UnitySetup-Facebook-Games-Support-for-Editor-$Version.$installerExtension"; - [UnitySetupComponent]::Linux = , "$targetSupport/UnitySetup-Linux-Support-for-Editor-$Version.$installerExtension"; - [UnitySetupComponent]::Mac = "$targetSupport/UnitySetup-Mac-Support-for-Editor-$Version.$installerExtension", - "$targetSupport/UnitySetup-Mac-Mono-Support-for-Editor-$Version.$installerExtension"; - [UnitySetupComponent]::Vuforia = , "$targetSupport/UnitySetup-Vuforia-AR-Support-for-Editor-$Version.$installerExtension"; - [UnitySetupComponent]::WebGL = , "$targetSupport/UnitySetup-WebGL-Support-for-Editor-$Version.$installerExtension"; + [UnitySetupComponent]::UWP = "$targetSupport/UnitySetup-UWP-.NET-Support-for-Editor-$Version.$installerExtension", + "$targetSupport/UnitySetup-Metro-Support-for-Editor-$Version.$installerExtension", + "$targetSupport/UnitySetup-Universal-Windows-Platform-Support-for-Editor-$Version.$installerExtension"; + [UnitySetupComponent]::UWP_IL2CPP = , "$targetSupport/UnitySetup-UWP-IL2CPP-Support-for-Editor-$Version.$installerExtension"; + [UnitySetupComponent]::Android = , "$targetSupport/UnitySetup-Android-Support-for-Editor-$Version.$installerExtension"; + [UnitySetupComponent]::iOS = , "$targetSupport/UnitySetup-iOS-Support-for-Editor-$Version.$installerExtension"; + [UnitySetupComponent]::AppleTV = , "$targetSupport/UnitySetup-AppleTV-Support-for-Editor-$Version.$installerExtension"; + [UnitySetupComponent]::Facebook = , "$targetSupport/UnitySetup-Facebook-Games-Support-for-Editor-$Version.$installerExtension"; + [UnitySetupComponent]::Linux = , "$targetSupport/UnitySetup-Linux-Support-for-Editor-$Version.$installerExtension"; + [UnitySetupComponent]::Mac = "$targetSupport/UnitySetup-Mac-Support-for-Editor-$Version.$installerExtension", + "$targetSupport/UnitySetup-Mac-Mono-Support-for-Editor-$Version.$installerExtension"; + [UnitySetupComponent]::Mac_IL2CPP = , "$targetSupport/UnitySetup-Mac-IL2CPP-Support-for-Editor-$Version.$installerExtension"; + [UnitySetupComponent]::Vuforia = , "$targetSupport/UnitySetup-Vuforia-AR-Support-for-Editor-$Version.$installerExtension"; + [UnitySetupComponent]::WebGL = , "$targetSupport/UnitySetup-WebGL-Support-for-Editor-$Version.$installerExtension"; [UnitySetupComponent]::Windows_IL2CPP = , "$targetSupport/UnitySetup-Windows-IL2CPP-Support-for-Editor-$Version.$installerExtension"; + [UnitySetupComponent]::Lumin = , "$targetSupport/UnitySetup-Lumin-Support-for-Editor-$Version.$installerExtension"; } switch ($currentOS) { @@ -346,26 +404,55 @@ function Find-UnitySetupInstaller { # Every release type has a different pattern for finding installers $searchPages = @() switch ($Version.Release) { - 'f' { $searchPages += "https://unity3d.com/get-unity/download/archive" } - 'b' { $searchPages += "https://unity3d.com/unity/beta/unity$Version" } + 'a' { $searchPages += "https://unity3d.com/alpha/$($Version.Major).$($Version.Minor)" } + 'b' { + $searchPages += "https://unity3d.com/unity/beta/unity$Version", + "https://unity3d.com/unity/beta/$($Version.Major).$($Version.Minor)", + "https://unity3d.com/unity/beta/$Version" + } + 'f' { + $searchPages += "https://unity3d.com/get-unity/download/archive", + "https://unity3d.com/unity/whats-new/$($Version.Major).$($Version.Minor).$($Version.Revision)" + + # Just in case it's a release candidate search the beta as well. + if ($Version.Revision -eq '0') { + $searchPages += "https://unity3d.com/unity/beta/unity$Version", + "https://unity3d.com/unity/beta/$($Version.Major).$($Version.Minor)", + "https://unity3d.com/unity/beta/$Version" + } + } 'p' { $patchPage = "https://unity3d.com/unity/qa/patch-releases?version=$($Version.Major).$($Version.Minor)" $searchPages += $patchPage - $webResult = Invoke-WebRequest $patchPage -UseBasicParsing - $searchPages += $webResult.Links | Where-Object { + $webResult = Invoke-WebRequest $patchPage -UseBasicParsing + $searchPages += $webResult.Links | Where-Object { $_.href -match "\/unity\/qa\/patch-releases\?version=$($Version.Major)\.$($Version.Minor)&page=(\d+)" -and $Matches[1] -gt 1 } | ForEach-Object { "https://unity3d.com$($_.href)" } } } foreach ($page in $searchPages) { - $webResult = Invoke-WebRequest $page -UseBasicParsing - $prototypeLink = $webResult.Links | Select-Object -ExpandProperty href -ErrorAction SilentlyContinue | Where-Object { - $_ -match "$($installerTemplates[$setupComponent])$" - } + try { + $webResult = Invoke-WebRequest $page -UseBasicParsing + $prototypeLink = $webResult.Links | Select-Object -ExpandProperty href -ErrorAction SilentlyContinue | Where-Object { + $link = $_ + + foreach ( $installer in $installerTemplates.Keys ) { + foreach ( $template in $installerTemplates[$installer] ) { + if ( $link -like "*$template*" ) { return $true } + } + } + + return $false - if ($null -ne $prototypeLink) { break } + } | Select-Object -First 1 + + if ($null -ne $prototypeLink) { break } + } + catch { + Write-Verbose "$page failed: $($_.Exception.Message)" + } } if ($null -eq $prototypeLink) { @@ -404,16 +491,16 @@ function Find-UnitySetupInstaller { } $result = New-Object UnitySetupInstaller -Property @{ 'ComponentType' = $_; - 'Version' = $Version; - 'DownloadUrl' = $endpoint; - 'Length' = $installerLength; - 'LastModified' = $lastModified; + 'Version' = $Version; + 'DownloadUrl' = $endpoint; + 'Length' = $installerLength; + 'LastModified' = $lastModified; } break } catch { - Write-Verbose "$endpoint failed: $_" + Write-Verbose "$endpoint failed: $($_.Exception.Message)" } } @@ -427,6 +514,370 @@ function Find-UnitySetupInstaller { } | Sort-Object -Property ComponentType } +<# +.Synopsis + Test if a Unity instance is installed. +.DESCRIPTION + Returns the status of a Unity install by Version and/or Path to install. +.PARAMETER Version + What version of Unity are you looking for? +.PARAMETER BasePath + Under what base patterns is Unity customly installed at. +.PARAMETER Path + Exact path you expect Unity to be installed at. +.EXAMPLE + Test-UnitySetupInstance -Version 2017.3.0f3 +.EXAMPLE + Test-UnitySetupInstance -BasePath D:/UnityInstalls/Unity2018 +#> +function Test-UnitySetupInstance { + [CmdletBinding()] + param( + [parameter(Mandatory = $false)] + [UnityVersion] $Version, + + [parameter(Mandatory = $false)] + [string] $BasePath, + + [parameter(Mandatory = $false)] + [string] $Path + ) + + $instance = Get-UnitySetupInstance -BasePath $BasePath | Select-UnitySetupInstance -Version $Version -Path $Path + return $null -ne $instance +} + +<# +.Synopsis + Select installers by a version and/or components. +.DESCRIPTION + Filters a list of `UnitySetupInstaller` down to a specific version and/or specific components. +.PARAMETER Installers + List of installers that needs to be reduced. +.PARAMETER Version + What version of UnitySetupInstaller that you want to keep. +.PARAMETER Components + What components should be maintained. +.EXAMPLE + $installers = Find-UnitySetupInstaller -Version 2017.3.0f3 + $installers += Find-UnitySetupInstaller -Version 2018.2.5f1 + $installers | Select-UnitySetupInstaller -Component Windows,Linux,Mac +#> +function Select-UnitySetupInstaller { + [CmdletBinding()] + param( + [parameter(ValueFromPipeline = $true)] + [UnitySetupInstaller[]] $Installers, + + [parameter(Mandatory = $false)] + [UnityVersion] $Version, + + [parameter(Mandatory = $false)] + [UnitySetupComponent] $Components = [UnitySetupComponent]::All + ) + begin { + $selectedInstallers = @() + } + process { + # Keep only the matching version specified. + if ( $PSBoundParameters.ContainsKey('Version') ) { + $Installers = $Installers | Where-Object { [UnityVersion]::Compare($_.Version, $Version) -eq 0 } + } + + # Keep only the matching component(s). + $Installers = $Installers | Where-Object { $Components -band $_.ComponentType } | ForEach-Object { $_ } + + if ($Installers.Length -ne 0) { + $selectedInstallers += $Installers + } + } + end { + return $selectedInstallers + } +} + +filter Format-Bytes { + return "{0:N2} {1}" -f $( + if ($_ -lt 1kb) { $_, 'Bytes' } + elseif ($_ -lt 1mb) { ($_ / 1kb), 'KB' } + elseif ($_ -lt 1gb) { ($_ / 1mb), 'MB' } + elseif ($_ -lt 1tb) { ($_ / 1gb), 'GB' } + elseif ($_ -lt 1pb) { ($_ / 1tb), 'TB' } + else { ($_ / 1pb), 'PB' } + ) +} + +function Format-BitsPerSecond { + [CmdletBinding()] + param( + [parameter(Mandatory = $true)] + [int] $Bytes, + + [parameter(Mandatory = $true)] + [int] $Seconds + ) + if ($Seconds -le 0.001) { + return "0 Bps" + } + # Convert from bytes to bits + $Bits = ($Bytes * 8) / $Seconds + return "{0:N2} {1}" -f $( + if ($Bits -lt 1kb) { $Bits, 'Bps' } + elseif ($Bits -lt 1mb) { ($Bits / 1kb), 'Kbps' } + elseif ($Bits -lt 1gb) { ($Bits / 1mb), 'Mbps' } + elseif ($Bits -lt 1tb) { ($Bits / 1gb), 'Gbps' } + elseif ($Bits -lt 1pb) { ($Bits / 1tb), 'Tbps' } + else { ($Bits / 1pb), 'Pbps' } + ) +} + +<# +.Synopsis + Download specified Unity installers. +.DESCRIPTION + Downloads the given installers into the $Cache directory. +.PARAMETER Installers + List of installers that needs to be downloaded. +.PARAMETER Cache + File path where installers will be downloaded to. +.EXAMPLE + $installers = Find-UnitySetupInstaller -Version 2017.3.0f3 + Request-UnitySetupInstaller -Installers $installers +.EXAMPLE + Find-UnitySetupInstaller -Version 2017.3.0f3 | Request-UnitySetupInstaller +#> +function Request-UnitySetupInstaller { + [CmdletBinding()] + param( + [parameter(ValueFromPipeline = $true)] + [UnitySetupInstaller[]] $Installers, + + [parameter(Mandatory = $false)] + [string]$Cache = [io.Path]::Combine("~", ".unitysetup") + ) + begin { + # Note that this has to happen before calculating the full path since + # Resolve-Path throws an exception on missing paths. + if (!(Test-Path $Cache -PathType Container)) { + New-Item $Cache -ItemType Directory -ErrorAction Stop | Out-Null + } + + # Expanding '~' to the absolute path on the system. `WebClient` on macOS asumes + # relative path. macOS also treats alt directory separators as part of the file + # name and this corrects the separators to current environment. + $fullCachePath = (Resolve-Path -Path $Cache).Path + + $allInstallers = @() + } + process { + # Append the full list of installers to enable batch downloading of installers. + $Installers | ForEach-Object { + $allInstallers += , $_ + } + } + end { + $downloads = @() + + try { + $global:downloadData = [ordered]@{} + $downloadIndex = 1 + + $allInstallers | ForEach-Object { + $installerFileName = [io.Path]::GetFileName($_.DownloadUrl) + $destination = [io.Path]::Combine($fullCachePath, "Installers", "Unity-$($_.Version)", "$installerFileName") + + # Already downloaded? + if ( Test-Path $destination ) { + $destinationItem = Get-Item $destination + if ( ($destinationItem.Length -eq $_.Length ) -and + ($destinationItem.LastWriteTime -eq $_.LastModified) ) { + Write-Verbose "Skipping download because it's already in the cache: $($_.DownloadUrl)" + + $resource = New-Object UnitySetupResource -Property @{ + 'ComponentType' = $_.ComponentType + 'Path' = $destination + } + $downloads += , $resource + return + } + } + + $destinationDirectory = [io.path]::GetDirectoryName($destination) + if (!(Test-Path $destinationDirectory -PathType Container)) { + New-Item "$destinationDirectory" -ItemType Directory | Out-Null + } + + $webClient = New-Object System.Net.WebClient + + ++$downloadIndex + $global:downloadData[$installerFileName] = New-Object PSObject -Property @{ + installerFileName = $installerFileName + startTime = Get-Date + totalBytes = $_.Length + receivedBytes = 0 + isDownloaded = $false + destination = $destination + lastModified = $_.LastModified + componentType = $_.ComponentType + webClient = $webClient + downloadIndex = $downloadIndex + } + + # Register to events for showing progress of file download. + Register-ObjectEvent -InputObject $webClient -EventName DownloadProgressChanged -SourceIdentifier "$installerFileName-Changed" -MessageData $installerFileName -Action { + $global:downloadData[$event.MessageData].receivedBytes = $event.SourceArgs.BytesReceived + } | Out-Null + Register-ObjectEvent -InputObject $webClient -EventName DownloadFileCompleted -SourceIdentifier "$installerFileName-Completed" -MessageData $installerFileName -Action { + $global:downloadData[$event.MessageData].isDownloaded = $true + } | Out-Null + + try { + Write-Verbose "Downloading $($_.DownloadUrl) to $destination" + $webClient.DownloadFileAsync($_.DownloadUrl, $destination) + } + catch [System.Net.WebException] { + Write-Error "Failed downloading $installerFileName - $($_.Exception.Message)" + $global:downloadData.Remove($installerFileName) + + Unregister-Event -SourceIdentifier "$installerFileName-Completed" -Force + Unregister-Event -SourceIdentifier "$installerFileName-Changed" -Force + + $webClient.Dispose() + } + } + + # Showing progress of all file downloads + $totalDownloads = $global:downloadData.Count + do { + $installersDownloaded = 0 + + $global:downloadData.Keys | ForEach-Object { + $installerFileName = $_ + $data = $global:downloadData[$installerFileName] + + # Finished downloading + if ($null -eq $data.webClient) { + ++$installersDownloaded + return + } + if ($data.isDownloaded) { + Write-Progress -Activity "Downloading $installerFileName" -Status "Done" -Completed ` + -Id $data.downloadIndex + + Unregister-Event -SourceIdentifier "$installerFileName-Completed" -Force + Unregister-Event -SourceIdentifier "$installerFileName-Changed" -Force + + $data.webClient.Dispose() + $data.webClient = $null + + # Re-writes the last modified time for ensuring downloads are cached properly. + $downloadedFile = Get-Item $data.destination + $downloadedFile.LastWriteTime = $data.lastModified + + $resource = New-Object UnitySetupResource -Property @{ + 'ComponentType' = $data.componentType + 'Path' = $data.destination + } + $downloads += , $resource + return + } + + $elapsedTime = (Get-Date) - $data.startTime + + $progress = [int](($data.receivedBytes / [double]$data.totalBytes) * 100) + + $averageSpeed = $data.receivedBytes / $elapsedTime.TotalSeconds + $secondsRemaining = ($data.totalBytes - $data.receivedBytes) / $averageSpeed + + if ([double]::IsInfinity($secondsRemaining)) { + $averageSpeed = 0 + # -1 for Write-Progress prevents seconds remaining from showing. + $secondsRemaining = -1 + } + + $downloadSpeed = Format-BitsPerSecond -Bytes $data.receivedBytes -Seconds $elapsedTime.TotalSeconds + + Write-Progress -Activity "Downloading $installerFileName | $downloadSpeed" ` + -Status "$($data.receivedBytes | Format-Bytes) of $($data.totalBytes | Format-Bytes)" ` + -SecondsRemaining $secondsRemaining ` + -PercentComplete $progress ` + -Id $data.downloadIndex + } + } while ($installersDownloaded -lt $totalDownloads) + } + finally { + # If the script is stopped, e.g. Ctrl+C, we want to cancel any remaining downloads + $global:downloadData.Keys | ForEach-Object { + $installerFileName = $_ + $data = $global:downloadData[$installerFileName] + + if ($null -ne $data.webClient) { + if (-not $data.isDownloaded) { + $data.webClient.CancelAsync() + } + + Unregister-Event -SourceIdentifier "$installerFileName-Completed" -Force + Unregister-Event -SourceIdentifier "$installerFileName-Changed" -Force + + $data.webClient.Dispose() + $data.webClient = $null + } + } + + Remove-Variable -Name downloadData -Scope Global + } + + return $downloads + } +} + +function Install-UnitySetupPackage { + [CmdletBinding()] + param( + [parameter(Mandatory = $true)] + [UnitySetupResource] $Package, + + [parameter(Mandatory = $true)] + [string]$Destination + ) + + $currentOS = Get-OperatingSystem + switch ($currentOS) { + ([OperatingSystem]::Windows) { + $startProcessArgs = @{ + 'FilePath' = $Package.Path; + 'ArgumentList' = @("/S", "/D=$Destination"); + 'PassThru' = $true; + 'Wait' = $true; + } + } + ([OperatingSystem]::Linux) { + throw "Install-UnitySetupPackage has not been implemented on the Linux platform. Contributions welcomed!"; + } + ([OperatingSystem]::Mac) { + # Note that $Destination has to be a disk path. + # sudo installer -package $Package.Path -target / + $startProcessArgs = @{ + 'FilePath' = 'sudo'; + 'ArgumentList' = @("installer", "-package", $Package.Path, "-target", $Destination); + 'PassThru' = $true; + 'Wait' = $true; + } + } + } + + Write-Verbose "$(Get-Date): Installing $($Package.ComponentType) to $Destination." + $process = Start-Process @startProcessArgs + if ( $process ) { + if ( $process.ExitCode -ne 0) { + Write-Error "$(Get-Date): Failed with exit code: $($process.ExitCode)" + } + else { + Write-Verbose "$(Get-Date): Succeeded." + } + } +} + <# .Synopsis Installs a UnitySetup instance. @@ -434,14 +885,20 @@ function Find-UnitySetupInstaller { Downloads and installs UnitySetup installers found via Find-UnitySetupInstaller. .PARAMETER Installers What installers would you like to download and execute? +.PARAMETER BasePath + Under what base patterns is Unity customly installed at. .PARAMETER Destination Where would you like the UnitySetup instance installed? .PARAMETER Cache - Where should the installers be cached. This defaults to $env:USERPROFILE\.unitysetup. + Where should the installers be cached. This defaults to ~\.unitysetup. .EXAMPLE Find-UnitySetupInstaller -Version 2017.3.0f3 | Install-UnitySetupInstance .EXAMPLE Find-UnitySetupInstaller -Version 2017.3.0f3 | Install-UnitySetupInstance -Destination D:\Unity-2017.3.0f3 +.EXAMPLE + Find-UnitySetupInstaller -Version 2017.3.0f3 | Install-UnitySetupInstance -BasePath D:\UnitySetup\ +.EXAMPLE + Find-UnitySetupInstaller -Version 2017.3.0f3 | Install-UnitySetupInstance -BasePath D:\UnitySetup\ -Destination Unity-2017 #> function Install-UnitySetupInstance { [CmdletBinding()] @@ -449,95 +906,159 @@ function Install-UnitySetupInstance { [parameter(ValueFromPipeline = $true)] [UnitySetupInstaller[]] $Installers, + [parameter(Mandatory = $false)] + [string]$BasePath, + [parameter(Mandatory = $false)] [string]$Destination, [parameter(Mandatory = $false)] - [string]$Cache = [io.Path]::Combine($env:USERPROFILE, ".unitysetup") + [string]$Cache = [io.Path]::Combine("~", ".unitysetup") ) - - process { - if (!(Test-Path $Cache -PathType Container)) { - New-Item $Cache -ItemType Directory -ErrorAction Stop | Out-Null + begin { + $currentOS = Get-OperatingSystem + if ($currentOS -eq [OperatingSystem]::Linux) { + throw "Install-UnitySetupInstance has not been implemented on the Linux platform. Contributions welcomed!"; } - $localInstallers = @() - $localDestinations = @() - - $downloadSource = @() - $downloadDest = @() - foreach ( $i in $Installers) { - $fileName = [io.Path]::GetFileName($i.DownloadUrl) - $destPath = [io.Path]::Combine($Cache, "Installers\Unity-$($i.Version)\$fileName") + if ( -not $PSBoundParameters.ContainsKey('BasePath') ) { + $defaultInstallPath = switch ($currentOS) { + ([OperatingSystem]::Windows) { + "C:\Program Files\Unity\Hub\Editor\" + } + ([OperatingSystem]::Linux) { + throw "Install-UnitySetupInstance has not been implemented on the Linux platform. Contributions welcomed!"; + } + ([OperatingSystem]::Mac) { + "/Applications/Unity/Hub/Editor/" + } + } + } + else { + $defaultInstallPath = $BasePath + } - $localInstallers += , $destPath - if ($Destination) { - $localDestinations += , $Destination + $versionInstallers = @{} + } + process { + # Sort each installer received from the pipe into versions + $Installers | ForEach-Object { + $versionInstallers[$_.Version] += , $_ + } + } + end { + $versionInstallers.Keys | ForEach-Object { + $installVersion = $_ + $installerInstances = $versionInstallers[$installVersion] + + if ( $PSBoundParameters.ContainsKey('Destination') ) { + # Slight API change here. If BasePath is also provided treat Destination as a relative path. + if ( $PSBoundParameters.ContainsKey('BasePath') ) { + $installPath = $Destination + } + else { + $installPath = [io.path]::Combine($BasePath, $Destination) + } } else { - $localDestinations += , "C:\Program Files\Unity-$($i.Version)" + $installPath = [io.path]::Combine($defaultInstallPath, $installVersion) } - if ( Test-Path $destPath ) { - $destItem = Get-Item $destPath - if ( ($destItem.Length -eq $i.Length ) -and ($destItem.LastWriteTime -eq $i.LastModified) ) { - Write-Verbose "Skipping download because it's already in the cache: $($i.DownloadUrl)" - continue + if ($currentOS -eq [OperatingSystem]::Mac) { + $volumeRoot = "/Volumes/UnitySetup/" + $volumeInstallPath = [io.path]::Combine($volumeRoot, "Applications/Unity/") + + # Make sure the install path ends with a trailing slash. This + # is required in some commands to treat as directory. + if (-not $installPath.EndsWith([io.path]::DirectorySeparatorChar)) { + $installPath += [io.path]::DirectorySeparatorChar } - } - $downloadSource += $i.DownloadUrl - $downloadDest += $destPath - } + # Creating sparse bundle to host installing Unity in other locations + $unitySetupBundlePath = [io.path]::Combine($Cache, "UnitySetup.sparsebundle") + if (-not (Test-Path $unitySetupBundlePath)) { + Write-Verbose "Creating new sparse bundle disk image for installation." + & hdiutil create -size 32g -fs 'HFS+' -type 'SPARSEBUNDLE' -volname 'UnitySetup' $unitySetupBundlePath + } + Write-Verbose "Mounting sparse bundle disk." + & hdiutil mount $unitySetupBundlePath - if ( $downloadSource.Length -gt 0 ) { - [System.Net.WebClient[]]$webClients = @() - try { - for ($i = 0; $i -lt $downloadSource.Length; $i++) { - Write-Verbose "Downloading $($downloadSource[$i]) to $($downloadDest[$i])" - $destDirectory = [io.path]::GetDirectoryName($downloadDest[$i]) - if (!(Test-Path $destDirectory -PathType Container)) { - New-Item "$destDirectory" -ItemType Directory | Out-Null - } - - $webClient = New-Object System.Net.WebClient - $webClient.DownloadFileAsync($downloadSource[$i], $downloadDest[$i]) - $webClients += $webClient + # Previous version failed to remove. Cleaning up! + if (Test-Path $volumeInstallPath) { + Write-Verbose "Previous install did not clean up properly. Doing that now." + & sudo rm -Rf ([io.path]::Combine($volumeRoot, '*')) } - # Wait for all the downloads to finish - while( $webClients.Where({ $_.IsBusy }, 'First').Count -gt 0 ) {} + # Copy installed version back to the sparse bundle disk for Unity component installs. + if (Test-UnitySetupInstance -Path $installPath -BasePath $BasePath) { + Write-Verbose "Copying $installPath to $volumeInstallPath" - # Clear the list so the finally does no work - $webClients = @() + # Ensure the path exists before copying the previous version to the sparse bundle disk. + & mkdir -p $volumeInstallPath + + # Copy the files (-r) and recreate symlinks (-l) to the install directory. + # Preserve permissions (-p) and owner (-o). + # Need to mark the files with read permissions or installs may fail. + & sudo rsync -rlpo $installPath $volumeInstallPath --chmod=+r + } } - finally { - # If the script is stopped, e.g. Ctrl+C, we want to cancel any downloads - $webClients | ForEach-Object { $_.CancelAsync() } + + # TODO: Strip out components already installed in the destination. + + $installerPaths = $installerInstances | Request-UnitySetupInstaller -Cache $Cache + + # First install the Unity editor before other components. + $editorComponent = switch ($currentOS) { + ([OperatingSystem]::Windows) { [UnitySetupComponent]::Windows } + ([OperatingSystem]::Linux) { [UnitySetupComponent]::Linux } + ([OperatingSystem]::Mac) { [UnitySetupComponent]::Mac } } - - } - - for ($i = 0; $i -lt $localInstallers.Length; $i++) { - $installer = $localInstallers[$i] - $destination = $localDestinations[$i] - $startProcessArgs = @{ - 'FilePath' = $installer; - 'ArgumentList' = @("/S", "/D=$destination"); - 'PassThru' = $true; - 'Wait' = $true; + $packageDestination = $installPath + # Installers in macOS get installed to the sparse bundle disk first. + if ($currentOS -eq [OperatingSystem]::Mac) { + $packageDestination = $volumeRoot } - - Write-Verbose "$(Get-Date): Installing $installer to $destination." - $process = Start-Process @startProcessArgs - if ( $process ) { - if ( $process.ExitCode -ne 0) { - Write-Error "$(Get-Date): Failed with exit code: $($process.ExitCode)" + + $editorInstaller = $installerPaths | Where-Object { $_.ComponentType -band $editorComponent } + if ($null -ne $editorInstaller) { + Write-Verbose "Installing $($editorInstaller.ComponentType) Editor" + Install-UnitySetupPackage -Package $editorInstaller -Destination $packageDestination + } + + $installerPaths | ForEach-Object { + # Already installed this earlier. Skipping. + if ($_.ComponentType -band $editorComponent) { + return } - else { - Write-Verbose "$(Get-Date): Succeeded." + + Write-Verbose "Installing $($_.ComponentType)" + Install-UnitySetupPackage -Package $_ -Destination $packageDestination + } + + # Move the install from the sparse bundle disk to the install directory. + if ($currentOS -eq [OperatingSystem]::Mac) { + # rsync does not recursively create the directory path. + if (-not (Test-Path $installPath -PathType Container)) { + Write-Verbose "Creating directory $installPath." + New-Item $installPath -ItemType Directory -ErrorAction Stop | Out-Null } + + Write-Verbose "Copying install to $installPath." + # Copy the files (-r) and recreate symlinks (-l) to the install directory. + # Preserve permissions (-p) and owner (-o). + # chmod gives files read permissions. + & sudo rsync -rlpo $volumeInstallPath $installPath --chmod="+wr" --remove-source-files + + Write-Verbose "Freeing sparse bundle disk space and unmounting." + # Ensure the drive is cleaned up. + & sudo rm -Rf ([io.path]::Combine($volumeRoot, '*')) + + & hdiutil eject $volumeRoot + # Free up disk space since deleting items in the volume send them to the trash + # Also note that -batteryallowed enables compacting while not connected to + # power. The compact is quite quick since the volume is small. + & hdiutil compact $unitySetupBundlePath -batteryallowed } } } @@ -571,10 +1092,10 @@ function Uninstall-UnitySetupInstance { } $startProcessArgs = @{ - 'FilePath' = $uninstaller; - 'PassThru' = $true; - 'Wait' = $true; - 'ErrorAction' = 'Stop'; + 'FilePath' = $uninstaller; + 'PassThru' = $true; + 'Wait' = $true; + 'ErrorAction' = 'Stop'; 'ArgumentList' = @("/S"); } @@ -610,25 +1131,87 @@ function Get-UnitySetupInstance { if (-not $BasePath) { $BasePath = @('C:\Program Files*\Unity*', 'C:\Program Files\Unity\Hub\Editor\*') } - $ivyPath = 'Editor\Data\UnityExtensions\Unity\Networking\ivy.xml' } ([OperatingSystem]::Linux) { throw "Get-UnitySetupInstance has not been implemented on the Linux platform. Contributions welcomed!"; } ([OperatingSystem]::Mac) { if (-not $BasePath) { - $BasePath = @('/Applications/Unity*') + $BasePath = @('/Applications/Unity*', '/Applications/Unity/Hub/Editor/*') } - $ivyPath = 'Unity.app/Contents/UnityExtensions/Unity/Networking/ivy.xml' } } - foreach ( $folder in $BasePath ) { - $path = [io.path]::Combine("$folder", $ivyPath); + + $BasePath | Where-Object { Test-Path $_ -PathType Container } | + Get-ChildItem -Directory | Where-Object { (Get-UnityEditor $_.FullName).Count -gt 0 } | + ForEach-Object { + $path = $_.FullName + try { + Write-Verbose "Creating UnitySetupInstance for $path" + [UnitySetupInstance]::new($path) + } + catch { + Write-Warning "$_" + } + } +} + +<# +.Synopsis + Gets the UnityVersion for a UnitySetupInstance at Path +.DESCRIPTION + Given a set of unity setup instances, this will select the best one matching your requirements +.PARAMETER Path + Path to a UnitySetupInstance +.OUTPUTS + UnityVersion + Returns the UnityVersion for the UnitySetupInstance at Path, or nothing if there isn't one +.EXAMPLE + Get-UnitySetupInstanceVersion -Path 'C:\Program Files\Unity' +#> +function Get-UnitySetupInstanceVersion { + [CmdletBinding()] + param( + [ValidateNotNullOrEmpty()] + [ValidateScript( {Test-Path $_ -PathType Container})] + [Parameter(Mandatory = $true, Position = 0)] + [string]$Path + ) + + Write-Verbose "Attempting to find UnityVersion in $path" + + if ( Test-Path "$path\modules.json" -PathType Leaf ) { + + Write-Verbose "Searching $path\modules.json for module versions" + $modules = (Get-Content "$path\modules.json" -Raw) | ConvertFrom-Json + + foreach ( $module in $modules ) { + Write-Verbose "`tTesting DownloadUrl $($module.DownloadUrl)" + if ( $module.DownloadUrl -notmatch "(\d+)\.(\d+)\.(\d+)([fpab])(\d+)" ) { continue; } + + Write-Verbose "`tFound version!" + return [UnityVersion]$Matches[0] + } + } + + if ( Test-Path "$path\Editor" -PathType Container ) { + # We'll attempt to search for the version using the ivy.xml definitions for legacy editor compatibility. + + Write-Verbose "Looking for ivy.xml files under $path\Editor\" + $ivyFiles = Get-ChildItem -Path "$path\Editor\" -Filter 'ivy.xml' -Recurse -ErrorAction SilentlyContinue -Force -File + foreach ( $ivy in $ivyFiles) { + if ( $null -eq $ivy ) { continue; } + + Write-Verbose "`tLooking for version in $($ivy.FullName)" - Get-ChildItem $path -Recurse -ErrorAction Ignore | - ForEach-Object { - [UnitySetupInstance]::new((Join-Path $_.Directory "..\..\..\..\..\" | Convert-Path)) + [xml]$xmlDoc = Get-Content $ivy.FullName + + [string]$version = $xmlDoc.'ivy-module'.info.unityVersion + if ( -not $version ) { continue; } + + Write-Verbose "`tFound version!" + return [UnityVersion]$version } } } @@ -642,14 +1225,16 @@ function Get-UnitySetupInstance { Select the latest version available. .PARAMETER Version Select only instances matching Version. -.PARAMETER Project - Select only instances matching the version of the project at Project. +.PARAMETER Path + Select only instances matching the project at the provided path. .PARAMETER instances The list of instances to Select from. .EXAMPLE Get-UnitySetupInstance | Select-UnitySetupInstance -Latest .EXAMPLE Get-UnitySetupInstance | Select-UnitySetupInstance -Version 2017.1.0f3 +.EXAMPLE + Get-UnitySetupInstance | Select-UnitySetupInstance -Path (Get-Item /Applications/Unity*) #> function Select-UnitySetupInstance { [CmdletBinding()] @@ -660,11 +1245,21 @@ function Select-UnitySetupInstance { [parameter(Mandatory = $false)] [UnityVersion] $Version, + [parameter(Mandatory = $false)] + [string] $Path, + [parameter(Mandatory = $true, ValueFromPipeline = $true)] [UnitySetupInstance[]] $Instances ) process { + if ( $PSBoundParameters.ContainsKey('Path') ) { + $Path = $Path.TrimEnd([io.path]::DirectorySeparatorChar) + $Instances = $Instances | Where-Object { + $Path -eq (Get-Item $_.Path).FullName.TrimEnd([io.path]::DirectorySeparatorChar) + } + } + if ( $Version ) { $Instances = $Instances | Where-Object { [UnityVersion]::Compare($_.Version, $Version) -eq 0 } } @@ -706,10 +1301,10 @@ function Get-UnityProjectInstance { ) $args = @{ - 'Path' = $BasePath; - 'Filter' = 'ProjectSettings'; + 'Path' = $BasePath; + 'Filter' = 'ProjectSettings'; 'ErrorAction' = 'Ignore'; - 'Directory' = $true; + 'Directory' = $true; } if ( $Recurse ) { @@ -744,6 +1339,8 @@ function Get-UnityProjectInstance { Force operation as though $PWD is not a unity project. .PARAMETER ExecuteMethod The script method for the Unity Editor to execute. +.PARAMETER AdditionalArguments + Additional arguments for Unity or your custom method .PARAMETER OutputPath The output path that the Unity Editor should use. .PARAMETER LogFile @@ -766,6 +1363,12 @@ function Get-UnityProjectInstance { 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." .PARAMETER RunEditorTests 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." +.PARAMETER TestPlatform + The platform you want to run the tests on. Note that If unspecified, tests run in editmode by default. +.PARAMETER TestResults + The path indicating where Unity should save the result file. By default, Unity saves it in the Project’s root folder. +.PARAMETER RunTests + 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." .PARAMETER BatchMode Should the Unity Editor start in batch mode? .PARAMETER Quit @@ -779,7 +1382,7 @@ function Get-UnityProjectInstance { .EXAMPLE Start-UnityEditor -Version 2017.3.0f3 .EXAMPLE - Start-UnityEditor -ExecuteMethod Build.Invoke -BatchMode -Quit -LogFile .\build.log -Wait + Start-UnityEditor -ExecuteMethod Build.Invoke -BatchMode -Quit -LogFile .\build.log -Wait -AdditionalArguments "-BuildArg1 -BuildArg2" .EXAMPLE Get-UnityProjectInstance -Recurse | Start-UnityEditor -BatchMode -Quit .EXAMPLE @@ -809,6 +1412,8 @@ function Start-UnityEditor { [parameter(Mandatory = $false)] [string]$ExecuteMethod, [parameter(Mandatory = $false)] + [string]$AdditionalArguments, + [parameter(Mandatory = $false)] [string[]]$ExportPackage, [parameter(Mandatory = $false)] [string]$ImportPackage, @@ -819,7 +1424,7 @@ function Start-UnityEditor { [parameter(Mandatory = $false)] [string]$LogFile, [parameter(Mandatory = $false)] - [ValidateSet('StandaloneOSX', 'StandaloneWindows', 'iOS', 'Android', 'StandaloneLinux', 'StandaloneWindows64', 'WebGL', 'WSAPlayer', 'StandaloneLinux64', 'StandaloneLinuxUniversal', 'Tizen', 'PSP2', 'PS4', 'XBoxOne', 'N3DS', 'WiiU', 'tvOS', 'Switch')] + [ValidateSet('StandaloneOSX', 'StandaloneWindows', 'iOS', 'Android', 'StandaloneLinux', 'StandaloneWindows64', 'WebGL', 'WSAPlayer', 'StandaloneLinux64', 'StandaloneLinuxUniversal', 'Tizen', 'PSP2', 'PS4', 'XBoxOne', 'N3DS', 'WiiU', 'tvOS', 'Switch', 'Lumin')] [string]$BuildTarget, [parameter(Mandatory = $false)] [switch]$AcceptAPIUpdate, @@ -840,6 +1445,13 @@ function Start-UnityEditor { [parameter(Mandatory = $false)] [switch]$RunEditorTests, [parameter(Mandatory = $false)] + [ValidateSet('EditMode', 'PlayMode')] + [string]$TestPlatform, + [parameter(Mandatory = $false)] + [string]$TestResults, + [parameter(Mandatory = $false)] + [switch]$RunTests, + [parameter(Mandatory = $false)] [switch]$BatchMode, [parameter(Mandatory = $false)] [switch]$Quit, @@ -917,7 +1529,7 @@ function Start-UnityEditor { if ( -not $PSBoundParameters.ContainsKey('BatchMode') ) { $BatchMode = $true } if ( -not $PSBoundParameters.ContainsKey('Quit') ) { $Quit = $true } } - if ( $AcceptAPIUpdate ) { + if ( $AcceptAPIUpdate ) { $sharedArgs += '-accept-apiupdate' if ( -not $PSBoundParameters.ContainsKey('BatchMode')) { $BatchMode = $true } } @@ -935,7 +1547,11 @@ function Start-UnityEditor { if ( $EditorTestsFilter ) { $sharedArgs += '-editorTestsFilter', ($EditorTestsFilter -join ',') } if ( $EditorTestsResultFile ) { $sharedArgs += '-editorTestsResultFile', $EditorTestsResultFile } if ( $RunEditorTests ) { $sharedArgs += '-runEditorTests' } + if ( $TestPlatform ) { $sharedArgs += '-testPlatform', $TestPlatform } + if ( $TestResults ) { $sharedArgs += '-testResults', $TestResults } + if ( $RunTests ) { $sharedArgs += '-runTests' } if ( $ForceFree) { $sharedArgs += '-force-free' } + if ( $AdditionalArguments) { $sharedArgs += $AdditionalArguments } $instanceArgs = @() foreach ( $p in $projectInstances ) { @@ -963,60 +1579,29 @@ function Start-UnityEditor { } $projectPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($($p.Path)) - $instanceArgs += , ("-projectPath", $projectPath) + $instanceArgs += , ("-projectPath", "`"$projectPath`"") $setupInstances += , $setupInstance } - $currentOS = Get-OperatingSystem for ($i = 0; $i -lt $setupInstances.Length; $i++) { $setupInstance = $setupInstances[$i] - switch ($currentOS) { - ([OperatingSystem]::Windows) { - $editor = Get-ChildItem "$($setupInstance.Path)" -Filter 'Unity.exe' -Recurse | - Select-Object -First 1 -ExpandProperty FullName - - if ([string]::IsNullOrEmpty($editor)) { - Write-Error "Could not find Unity.exe under setup instance path: $($setupInstance.Path)" - continue - } - } - ([OperatingSystem]::Linux) { - throw "Start-UnityEditor has not been implemented on the Linux platform. Contributions welcomed!"; - } - ([OperatingSystem]::Mac) { - $editor = [io.path]::Combine("$($setupInstance.Path)", "Unity.app/Contents/MacOS/Unity") - - if ([string]::IsNullOrEmpty($editor)) { - Write-Error "Could not find Unity app under setup instance path: $($setupInstance.Path)" - continue - } - } + $editor = Get-UnityEditor "$($setupInstance.Path)" + if ( -not $editor ) { + Write-Error "Could not find Unity Editor under setup instance path: $($setupInstance.Path)" + continue } # clone the shared args list $unityArgs = $sharedArgs | ForEach-Object { $_ } if ( $instanceArgs[$i] ) { $unityArgs += $instanceArgs[$i] } - $setProcessArgs = @{ - 'FilePath' = $editor; - 'PassThru' = $true; - 'ErrorAction' = 'Stop'; - 'RedirectStandardOutput' = New-TemporaryFile; - 'RedirectStandardError' = New-TemporaryFile; - } - - if ($Wait) { $setProcessArgs['Wait'] = $true } - - Write-Verbose "Redirecting standard output to $($setProcessArgs['RedirectStandardOutput'])" - Write-Verbose "Redirecting standard error to $($setProcessArgs['RedirectStandardError'])" - $actionString = "$editor $unityArgs" - if( $Credential ) { $actionString += " -password (hidden)"} - if( $Serial ) { $actionString += " -serial (hidden)"} + if ( $Credential ) { $actionString += " -password (hidden)"} + if ( $Serial ) { $actionString += " -serial (hidden)"} - if (-not $PSCmdlet.ShouldProcess($actionString, "Start-Process")) { + if (-not $PSCmdlet.ShouldProcess($actionString, "System.Diagnostics.Process.Start()")) { continue } @@ -1024,17 +1609,27 @@ function Start-UnityEditor { if ( $Credential ) { $unityArgs += '-password', $Credential.GetNetworkCredential().Password } if ( $Serial ) { $unityArgs += '-serial', [System.Net.NetworkCredential]::new($null, $Serial).Password } - if ($unityArgs -and $unityArgs.Length -gt 0) { - $setProcessArgs['ArgumentList'] = $unityArgs - } + # We've experienced issues with Start-Process -Wait and redirecting + # output so we're using the Process class directly now. + $process = New-Object System.Diagnostics.Process + $process.StartInfo.Filename = $editor + $process.StartInfo.Arguments = $unityArgs + $process.StartInfo.RedirectStandardOutput = $true + $process.StartInfo.RedirectStandardError = $true + $process.StartInfo.UseShellExecute = $false + $process.StartInfo.CreateNoWindow = $true + $process.StartInfo.WorkingDirectory = $PWD + $process.StartInfo.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden + $process.Start() | Out-Null - $process = Start-Process @setProcessArgs if ( $Wait ) { + $process.WaitForExit() + if ( $LogFile -and (Test-Path $LogFile -Type Leaf) ) { # Note that Unity sometimes returns a success ExitCode despite the presence of errors, but we want # to make sure that we flag such errors. Write-UnityErrors $LogFile - + Write-Verbose "Writing $LogFile to Information stream Tagged as 'Logs'" Get-Content $LogFile | ForEach-Object { Write-Information -MessageData $_ -Tags 'Logs' } } @@ -1092,7 +1687,7 @@ function Get-IsUnityError { function ConvertTo-DateTime { param([string] $Text) - if( -not $text -or $text.Length -eq 0 ) { [DateTime]::MaxValue } + if ( -not $text -or $text.Length -eq 0 ) { [DateTime]::MaxValue } else { [DateTime]$Text } } @@ -1104,10 +1699,9 @@ function ConvertTo-DateTime { .EXAMPLE Get-UnityLicense #> -function Get-UnityLicense -{ +function Get-UnityLicense { [CmdletBinding()] - [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "", Justification="Used to convert discovered plaintext serials into secure strings.")] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "", Justification = "Used to convert discovered plaintext serials into secure strings.")] param([SecureString]$Serial) $licenseFiles = Get-ChildItem "C:\ProgramData\Unity\Unity_*.ulf" -ErrorAction 'SilentlyContinue' @@ -1118,18 +1712,18 @@ function Get-UnityLicense # The first four bytes look like a count so skip that to pull out the serial string $licenseSerial = [String]::new($devBytes[4..($devBytes.Length - 1)]) - if( $Serial -and [System.Net.NetworkCredential]::new($null, $Serial).Password -ne $licenseSerial ) { continue; } - + if ( $Serial -and [System.Net.NetworkCredential]::new($null, $Serial).Password -ne $licenseSerial ) { continue; } + $license = $doc.root.License [PSCustomObject]@{ 'LicenseVersion' = $license.LicenseVersion.Value - 'Serial' = ConvertTo-SecureString $licenseSerial -AsPlainText -Force - 'UnityVersion' = [UnityVersion]$license.ClientProvidedVersion.Value - 'DisplaySerial' = $license.SerialMasked.Value + 'Serial' = ConvertTo-SecureString $licenseSerial -AsPlainText -Force + 'UnityVersion' = [UnityVersion]$license.ClientProvidedVersion.Value + 'DisplaySerial' = $license.SerialMasked.Value 'ActivationDate' = ConvertTo-DateTime $license.InitialActivationDate.Value - 'StartDate' = ConvertTo-DateTime $license.StartDate.Value - 'StopDate' = ConvertTo-DateTime $license.StopDate.Value - 'UpdateDate' = ConvertTo-DateTime $license.UpdateDate.Value + 'StartDate' = ConvertTo-DateTime $license.StartDate.Value + 'StopDate' = ConvertTo-DateTime $license.StopDate.Value + 'UpdateDate' = ConvertTo-DateTime $license.UpdateDate.Value } } } @@ -1138,16 +1732,17 @@ function Get-UnityLicense @{ 'Name' = 'gusi'; 'Value' = 'Get-UnitySetupInstance' }, @{ 'Name' = 'gupi'; 'Value' = 'Get-UnityProjectInstance' }, @{ 'Name' = 'susi'; 'Value' = 'Select-UnitySetupInstance' }, + @{ 'Name' = 'gue'; 'Value' = 'Get-UnityEditor' } @{ 'Name' = 'sue'; 'Value' = 'Start-UnityEditor' } ) | ForEach-Object { $alias = Get-Alias -Name $_.Name -ErrorAction 'SilentlyContinue' if ( -not $alias ) { - Write-Verbose "Creating new alias $($_.Name) for $($_.Value)" - New-Alias @_ + Write-Verbose "Creating new alias $($_.Name) for $($_.Value)" + New-Alias @_ } elseif ( $alias.ModuleName -eq 'UnitySetup' ) { - Write-Verbose "Setting alias $($_.Name) to $($_.Value)" + Write-Verbose "Setting alias $($_.Name) to $($_.Value)" Set-Alias @_ } else {