feat(packages): 内置 SDK 包并完善 ContextLoader 集成

- 将 ShrinkEventBus、ShrinkDataSaver 及其 EventBus 集成从 gitlink 转为仓库直接维护的完整 UPM 包,补齐运行时、编辑器工具、测试与文档
- 新增 Command 和 Network 的 App 集成组件,支持 ContextLoader 服务发布、可逆注销及 Network Loopback 生命周期管理
- 更新 Starter 与演示组合逻辑,缺失模块时可注册、已有兼容安装器时可覆盖,并补充宿主启动断言
- 升级内部包依赖与 Shared CodeGen 包定义,放宽 Integration.App 包的 Git 忽略规则
- 将独立服务器生成器改为基于已编译程序集的语义扫描,支持 partial、复杂泛型、命名冲突检测及模板 SHA-256 覆写保护
- 新增 Network 语义扫描、模板保护和 App 组件生命周期测试
- 新增真实 UPM 消费工程验证脚本,校验内部版本一致性、程序集加载及 EditMode 测试
- 重构当前架构文档并归档已完成的 Cordis 迁移与旧代码地图
This commit is contained in:
2026-08-18 18:06:34 +08:00
parent 517c4cf46e
commit d74c2f08ca
240 changed files with 13647 additions and 545 deletions
+22
View File
@@ -0,0 +1,22 @@
# UPM Consumer Validation
`Validate-UpmConsumer.ps1` verifies the package graph from outside the main Unity
project. It performs three checks:
1. Every dependency on another local `com.cneicy.*` package must use that
package's current `package.json` version.
2. A temporary Unity project is created under `Temp/UpmConsumerValidation`, all
local packages are installed through `file:` UPM dependencies, package tests
are enabled, and Unity must load every package and asmdef.
3. The same temporary UPM consumer project runs all package EditMode tests.
Run with the Unity version declared by this repository:
```powershell
./Tools/UpmConsumerValidation/Validate-UpmConsumer.ps1
```
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
investigation. `-SkipTests` is only for a quick package-resolution diagnostic and is not the
release validation path.
@@ -0,0 +1,273 @@
[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 { '<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
}