327 lines
12 KiB
PowerShell
327 lines
12 KiB
PowerShell
[CmdletBinding()]
|
|
param(
|
|
[string]$UnityEditorPath,
|
|
[string]$ProjectRoot,
|
|
[switch]$KeepProject,
|
|
[switch]$SkipTests,
|
|
[switch]$GraphOnly
|
|
)
|
|
|
|
$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
|
|
}
|
|
|
|
$graphErrors = @()
|
|
$localDependencies = @{}
|
|
foreach ($record in $packageRecords) {
|
|
$dependencies = [System.Collections.Generic.HashSet[string]]::new(
|
|
[System.StringComparer]::Ordinal)
|
|
$localDependencies[$record.Name] = $dependencies
|
|
if (-not $record.Manifest.dependencies) {
|
|
continue
|
|
}
|
|
|
|
foreach ($dependency in $record.Manifest.dependencies.PSObject.Properties) {
|
|
if (-not $packageByName.ContainsKey($dependency.Name)) {
|
|
continue
|
|
}
|
|
|
|
[void]$dependencies.Add($dependency.Name)
|
|
$actualVersion = $packageByName[$dependency.Name].Version
|
|
if ([string]$dependency.Value -ne $actualVersion) {
|
|
$graphErrors += "$($record.Name) requires $($dependency.Name) $($dependency.Value), local version is $actualVersion"
|
|
}
|
|
|
|
$dependsOnIntegration = $dependency.Name -like '*-integration-*'
|
|
$isIntegrationOrStarter =
|
|
$record.Name -like '*-integration-*' -or
|
|
$record.Name -like '*-starter-*'
|
|
if ($dependsOnIntegration -and -not $isIntegrationOrStarter) {
|
|
$graphErrors += "$($record.Name) must not depend on integration package $($dependency.Name)"
|
|
}
|
|
}
|
|
}
|
|
if ($graphErrors.Count -gt 0) {
|
|
throw "Internal package graph is invalid:`n$($graphErrors -join "`n")"
|
|
}
|
|
|
|
$resolvedPackages = [System.Collections.Generic.HashSet[string]]::new(
|
|
[System.StringComparer]::Ordinal)
|
|
do {
|
|
$madeProgress = $false
|
|
foreach ($packageName in $localDependencies.Keys | Sort-Object) {
|
|
if ($resolvedPackages.Contains($packageName)) {
|
|
continue
|
|
}
|
|
|
|
$unresolvedDependencies = @($localDependencies[$packageName] |
|
|
Where-Object { -not $resolvedPackages.Contains($_) })
|
|
if ($unresolvedDependencies.Count -eq 0) {
|
|
[void]$resolvedPackages.Add($packageName)
|
|
$madeProgress = $true
|
|
}
|
|
}
|
|
} while ($madeProgress)
|
|
|
|
if ($resolvedPackages.Count -ne $packageRecords.Count) {
|
|
$cycleDetails = $localDependencies.Keys |
|
|
Where-Object { -not $resolvedPackages.Contains($_) } |
|
|
Sort-Object |
|
|
ForEach-Object {
|
|
$packageName = $_
|
|
$blockedBy = @($localDependencies[$packageName] |
|
|
Where-Object { -not $resolvedPackages.Contains($_) } |
|
|
Sort-Object)
|
|
"$packageName -> $($blockedBy -join ', ')"
|
|
}
|
|
throw "Circular internal package dependencies detected:`n$($cycleDetails -join "`n")"
|
|
}
|
|
|
|
Write-Host "Package graph PASS packages=$($packageRecords.Count)"
|
|
if ($GraphOnly) {
|
|
return
|
|
}
|
|
|
|
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#7c0f199fe0d3fc528024488ccd671e6c7b27745b'
|
|
'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'
|
|
'com.unity.modules.uielements' = '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 } |
|
|
Where-Object { $_.FullName -notmatch '[\\/][^\\/]+~[\\/]' } |
|
|
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 { '<no Unity log>' }
|
|
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 { '<no Unity test log>' }
|
|
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
|
|
}
|