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:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user