chore(validation): enforce package graph boundaries

Reject internal package version drift, dependency cycles, and runtime-to-integration reverse dependencies before launching the external UPM consumer validation.
This commit is contained in:
2026-08-26 01:18:02 +08:00
parent 724e0bc8d8
commit 3e612b975a
2 changed files with 69 additions and 8 deletions
+12 -3
View File
@@ -1,14 +1,17 @@
# UPM Consumer Validation # UPM Consumer Validation
`Validate-UpmConsumer.ps1` verifies the package graph from outside the main Unity `Validate-UpmConsumer.ps1` verifies the package graph from outside the main Unity
project. It performs three checks: project. It performs five checks:
1. Every dependency on another local `com.cneicy.*` package must use that 1. Every dependency on another local `com.cneicy.*` package must use that
package's current `package.json` version. package's current `package.json` version.
2. A temporary Unity project is created under `Temp/UpmConsumerValidation`, all 2. The local package graph must be acyclic.
3. A normal package must not depend on a `*-integration-*` package. Integration
packages may depend on each other, and starter packages may aggregate them.
4. A temporary Unity project is created under `Temp/UpmConsumerValidation`, all
local packages are installed through `file:` UPM dependencies, package tests local packages are installed through `file:` UPM dependencies, package tests
are enabled, and Unity must load every package and asmdef. are enabled, and Unity must load every package and asmdef.
3. The same temporary UPM consumer project runs all package EditMode tests. 5. The same temporary UPM consumer project runs all package EditMode tests.
Run with the Unity version declared by this repository: Run with the Unity version declared by this repository:
@@ -16,6 +19,12 @@ Run with the Unity version declared by this repository:
./Tools/UpmConsumerValidation/Validate-UpmConsumer.ps1 ./Tools/UpmConsumerValidation/Validate-UpmConsumer.ps1
``` ```
Run only the package graph checks without starting Unity:
```powershell
./Tools/UpmConsumerValidation/Validate-UpmConsumer.ps1 -GraphOnly
```
Use `-UnityEditorPath` when the matching editor is not installed in a standard Use `-UnityEditorPath` when the matching editor is not installed in a standard
location. Pass `-KeepProject` to retain the temporary project, reports, and Unity logs for location. Pass `-KeepProject` to retain the temporary project, reports, and Unity logs for
investigation. `-SkipTests` is only for a quick package-resolution diagnostic and is not the investigation. `-SkipTests` is only for a quick package-resolution diagnostic and is not the
@@ -3,7 +3,8 @@ param(
[string]$UnityEditorPath, [string]$UnityEditorPath,
[string]$ProjectRoot, [string]$ProjectRoot,
[switch]$KeepProject, [switch]$KeepProject,
[switch]$SkipTests [switch]$SkipTests,
[switch]$GraphOnly
) )
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
@@ -101,8 +102,12 @@ foreach ($record in $packageRecords) {
$packageByName[$record.Name] = $record $packageByName[$record.Name] = $record
} }
$versionErrors = @() $graphErrors = @()
$localDependencies = @{}
foreach ($record in $packageRecords) { foreach ($record in $packageRecords) {
$dependencies = [System.Collections.Generic.HashSet[string]]::new(
[System.StringComparer]::Ordinal)
$localDependencies[$record.Name] = $dependencies
if (-not $record.Manifest.dependencies) { if (-not $record.Manifest.dependencies) {
continue continue
} }
@@ -112,14 +117,60 @@ foreach ($record in $packageRecords) {
continue continue
} }
[void]$dependencies.Add($dependency.Name)
$actualVersion = $packageByName[$dependency.Name].Version $actualVersion = $packageByName[$dependency.Name].Version
if ([string]$dependency.Value -ne $actualVersion) { if ([string]$dependency.Value -ne $actualVersion) {
$versionErrors += "$($record.Name) requires $($dependency.Name) $($dependency.Value), local version is $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 ($versionErrors.Count -gt 0) { if ($graphErrors.Count -gt 0) {
throw "Internal package versions are inconsistent:`n$($versionErrors -join "`n")" 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) { if (Test-Path -LiteralPath $consumerRoot) {
@@ -133,6 +184,7 @@ $dependencies = [ordered]@{
'com.unity.test-framework' = '1.1.33' 'com.unity.test-framework' = '1.1.33'
'com.unity.textmeshpro' = '3.0.7' 'com.unity.textmeshpro' = '3.0.7'
'com.unity.ugui' = '1.0.0' 'com.unity.ugui' = '1.0.0'
'com.unity.modules.uielements' = '1.0.0'
} }
foreach ($record in $packageRecords | Sort-Object Name) { foreach ($record in $packageRecords | Sort-Object Name) {
$dependencies[$record.Name] = Convert-ToFileDependency $record.Directory $dependencies[$record.Name] = Convert-ToFileDependency $record.Directory