[CmdletBinding()] param( [string]$UnityEditorPath, [string]$ProjectRoot, [switch]$KeepProject, [switch]$SkipTests ) $ErrorActionPreference = 'Stop' function Resolve-UnityEditor { param([string]$RequestedPath) if ($RequestedPath) { $resolved = Resolve-Path -LiteralPath $RequestedPath -ErrorAction Stop return $resolved.Path } $projectVersionPath = Join-Path $script:ResolvedProjectRoot 'ProjectSettings/ProjectVersion.txt' $versionLine = Get-Content -LiteralPath $projectVersionPath | Where-Object { $_ -like 'm_EditorVersion:*' } | Select-Object -First 1 if (-not $versionLine) { throw "Unity version was not found in $projectVersionPath" } $version = ($versionLine -split ':', 2)[1].Trim() $running = Get-Process Unity -ErrorAction SilentlyContinue | Where-Object { $_.Path -and $_.Path -like "*$version*" } | Select-Object -First 1 if ($running) { return $running.Path } $candidates = @( "D:/UnityEditor/$version/Editor/Unity.exe", "C:/Program Files/Unity/Hub/Editor/$version/Editor/Unity.exe", "C:/Program Files/Unity Hub/Editor/$version/Editor/Unity.exe" ) foreach ($candidate in $candidates) { if (Test-Path -LiteralPath $candidate) { return (Resolve-Path -LiteralPath $candidate).Path } } throw "Unity $version was not found. Pass -UnityEditorPath explicitly." } function Convert-ToFileDependency { param([string]$Path) return 'file:' + ([System.IO.Path]::GetFullPath($Path).Replace('\', '/')) } function Convert-ToCSharpLiteral { param([string]$Value) return '"' + $Value.Replace('\', '\\').Replace('"', '\"') + '"' } if (-not $ProjectRoot) { $ProjectRoot = Join-Path $PSScriptRoot '../..' } $script:ResolvedProjectRoot = (Resolve-Path -LiteralPath $ProjectRoot).Path $modulesRoot = Join-Path $script:ResolvedProjectRoot 'Assets/Modules' $tempRoot = Join-Path $script:ResolvedProjectRoot 'Temp' $consumerRoot = Join-Path $tempRoot 'UpmConsumerValidation' $packagesPath = Join-Path $consumerRoot 'Packages' $assetsEditorPath = Join-Path $consumerRoot 'Assets/Editor' $projectSettingsPath = Join-Path $consumerRoot 'ProjectSettings' $reportPath = Join-Path $consumerRoot 'upm-consumer-report.txt' $logPath = Join-Path $consumerRoot 'unity-upm-consumer.log' $testResultsPath = Join-Path $consumerRoot 'editmode-results.xml' $testLogPath = Join-Path $consumerRoot 'unity-editmode-tests.log' $resolvedTempRoot = [System.IO.Path]::GetFullPath($tempRoot).TrimEnd('\') + '\' $resolvedConsumerRoot = [System.IO.Path]::GetFullPath($consumerRoot) if (-not $resolvedConsumerRoot.StartsWith($resolvedTempRoot, [System.StringComparison]::OrdinalIgnoreCase)) { throw "Consumer project must stay under the repository Temp directory: $resolvedConsumerRoot" } $packageRecords = @() foreach ($directory in Get-ChildItem -LiteralPath $modulesRoot -Directory) { $manifestPath = Join-Path $directory.FullName 'package.json' if (-not (Test-Path -LiteralPath $manifestPath)) { continue } $manifest = Get-Content -Raw -LiteralPath $manifestPath | ConvertFrom-Json $packageRecords += [PSCustomObject]@{ Directory = $directory.FullName Name = [string]$manifest.name Version = [string]$manifest.version Manifest = $manifest } } $packageByName = @{} foreach ($record in $packageRecords) { if ($packageByName.ContainsKey($record.Name)) { throw "Duplicate package name: $($record.Name)" } $packageByName[$record.Name] = $record } $versionErrors = @() foreach ($record in $packageRecords) { if (-not $record.Manifest.dependencies) { continue } foreach ($dependency in $record.Manifest.dependencies.PSObject.Properties) { if (-not $packageByName.ContainsKey($dependency.Name)) { continue } $actualVersion = $packageByName[$dependency.Name].Version if ([string]$dependency.Value -ne $actualVersion) { $versionErrors += "$($record.Name) requires $($dependency.Name) $($dependency.Value), local version is $actualVersion" } } } if ($versionErrors.Count -gt 0) { throw "Internal package versions are inconsistent:`n$($versionErrors -join "`n")" } if (Test-Path -LiteralPath $consumerRoot) { Remove-Item -LiteralPath $consumerRoot -Recurse -Force } New-Item -ItemType Directory -Path $packagesPath, $assetsEditorPath, $projectSettingsPath -Force | Out-Null $dependencies = [ordered]@{ 'com.cysharp.unitask' = 'https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask' 'com.unity.nuget.newtonsoft-json' = '3.2.2' 'com.unity.test-framework' = '1.1.33' 'com.unity.textmeshpro' = '3.0.7' 'com.unity.ugui' = '1.0.0' } foreach ($record in $packageRecords | Sort-Object Name) { $dependencies[$record.Name] = Convert-ToFileDependency $record.Directory } $consumerManifest = [ordered]@{ dependencies = $dependencies testables = @($packageRecords.Name | Sort-Object) } $consumerManifest | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $packagesPath 'manifest.json') -Encoding utf8 Copy-Item -LiteralPath (Join-Path $script:ResolvedProjectRoot 'ProjectSettings/ProjectVersion.txt') ` -Destination (Join-Path $projectSettingsPath 'ProjectVersion.txt') $expectedPackages = ($packageRecords.Name | Sort-Object | ForEach-Object { Convert-ToCSharpLiteral $_ }) -join ",`n " $expectedAssemblies = $packageRecords | ForEach-Object { Get-ChildItem -LiteralPath $_.Directory -Recurse -Filter '*.asmdef' -File } | ForEach-Object { (Get-Content -Raw -LiteralPath $_.FullName | ConvertFrom-Json).name } | Sort-Object -Unique | ForEach-Object { Convert-ToCSharpLiteral $_ } $expectedAssemblies = $expectedAssemblies -join ",`n " $escapedReportPath = Convert-ToCSharpLiteral $reportPath $validatorSource = @" using System; using System.IO; using System.Linq; using UnityEditor; using UnityEditor.Compilation; using UnityEditor.PackageManager; public static class UpmConsumerValidator { private static readonly string[] ExpectedPackages = { $expectedPackages }; private static readonly string[] ExpectedAssemblies = { $expectedAssemblies }; public static void Run() { try { var installed = UnityEditor.PackageManager.PackageInfo.GetAllRegisteredPackages() .Select(package => package.name) .ToHashSet(StringComparer.Ordinal); var assemblies = CompilationPipeline.GetAssemblies() .Select(assembly => assembly.name) .ToHashSet(StringComparer.Ordinal); var missingPackages = ExpectedPackages.Where(name => !installed.Contains(name)).ToArray(); var missingAssemblies = ExpectedAssemblies.Where(name => !assemblies.Contains(name)).ToArray(); if (missingPackages.Length > 0 || missingAssemblies.Length > 0) { throw new InvalidOperationException( "Missing packages: " + string.Join(", ", missingPackages) + Environment.NewLine + "Missing assemblies: " + string.Join(", ", missingAssemblies)); } File.WriteAllText($escapedReportPath, "PASS" + Environment.NewLine + "packages=" + ExpectedPackages.Length + Environment.NewLine + "assemblies=" + ExpectedAssemblies.Length + Environment.NewLine); EditorApplication.Exit(0); } catch (Exception exception) { File.WriteAllText($escapedReportPath, "FAIL" + Environment.NewLine + exception); UnityEngine.Debug.LogException(exception); EditorApplication.Exit(1); } } } "@ $validatorSource | Set-Content -LiteralPath (Join-Path $assetsEditorPath 'UpmConsumerValidator.cs') -Encoding utf8 $resolvedUnity = Resolve-UnityEditor $UnityEditorPath Write-Host "Validating $($packageRecords.Count) local UPM packages with $resolvedUnity" $validationProcess = Start-Process -FilePath $resolvedUnity -ArgumentList @( '-batchmode', '-nographics', '-projectPath', $consumerRoot, '-executeMethod', 'UpmConsumerValidator.Run', '-quit', '-logFile', $logPath ) -WindowStyle Hidden -Wait -PassThru $unityExitCode = $validationProcess.ExitCode if (-not (Test-Path -LiteralPath $reportPath)) { $logTail = if (Test-Path -LiteralPath $logPath) { (Get-Content -LiteralPath $logPath -Tail 120) -join "`n" } else { '' } throw "UPM consumer validator did not produce a report (Unity exit $unityExitCode).`n$logTail" } $report = Get-Content -Raw -LiteralPath $reportPath if ($unityExitCode -ne 0 -or -not $report.StartsWith('PASS', [System.StringComparison]::Ordinal)) { $logTail = (Get-Content -LiteralPath $logPath -Tail 120) -join "`n" throw "UPM consumer validation failed (Unity exit $unityExitCode).`n$report`n$logTail" } Write-Host $report.Trim() Write-Host "Unity log: $logPath" if (-not $SkipTests) { Write-Host 'Running EditMode tests from the real UPM consumer project' $testProcess = Start-Process -FilePath $resolvedUnity -ArgumentList @( '-batchmode', '-nographics', '-projectPath', $consumerRoot, '-runTests', '-testPlatform', 'EditMode', '-testResults', $testResultsPath, '-logFile', $testLogPath ) -WindowStyle Hidden -Wait -PassThru $testExitCode = $testProcess.ExitCode if (-not (Test-Path -LiteralPath $testResultsPath)) { $testLogTail = if (Test-Path -LiteralPath $testLogPath) { (Get-Content -LiteralPath $testLogPath -Tail 160) -join "`n" } else { '' } throw "Unity Test Runner did not produce results (Unity exit $testExitCode).`n$testLogTail" } [xml]$testResults = Get-Content -Raw -LiteralPath $testResultsPath $testRun = $testResults.'test-run' if ($testExitCode -ne 0 -or [string]$testRun.result -ne 'Passed' -or [int]$testRun.failed -ne 0) { $testLogTail = (Get-Content -LiteralPath $testLogPath -Tail 160) -join "`n" throw "UPM consumer EditMode tests failed (Unity exit $testExitCode, result=$($testRun.result), total=$($testRun.total), failed=$($testRun.failed)).`n$testLogTail" } Write-Host "EditMode PASS total=$($testRun.total) passed=$($testRun.passed)" Write-Host "Test results: $testResultsPath" Write-Host "Test log: $testLogPath" } if (-not $KeepProject) { Remove-Item -LiteralPath $consumerRoot -Recurse -Force }