feat(workspace): add package installation validation
Validate ShrinkSDK Workspace / unity (push) Failing after 8s
Validate ShrinkSDK Workspace / unity (push) Failing after 8s
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fa5cd8919ec4f544b89255c3da6b4ba0
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,190 @@
|
||||
#if UNITY_EDITOR
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ShrinkSDK.WorkspaceValidation
|
||||
{
|
||||
public static class ShrinkSdkWorkspaceValidation
|
||||
{
|
||||
private static readonly ExpectedPackage[] ExpectedPackages =
|
||||
{
|
||||
new ExpectedPackage("ShrinkApp.Core", "com.cneicy.shrink-app-core", "0.1.1"),
|
||||
new ExpectedPackage("ShrinkApp.Starter.Basic", "com.cneicy.shrink-app-starter-basic", "0.1.0"),
|
||||
new ExpectedPackage("ShrinkCommand", "com.cneicy.shrink-command", "0.2.0"),
|
||||
new ExpectedPackage("ShrinkCommand.Integration.App", "com.cneicy.shrink-command-integration-app", "0.1.0"),
|
||||
new ExpectedPackage("ShrinkCommand.Integration.EventBus", "com.cneicy.shrink-command-integration-eventbus", "0.1.1"),
|
||||
new ExpectedPackage("ShrinkCommand.Integration.Network", "com.cneicy.shrink-command-integration-network", "0.1.0"),
|
||||
new ExpectedPackage("ShrinkContext.AppAdapter", "com.cneicy.shrink-context-app-adapter", "0.1.0"),
|
||||
new ExpectedPackage("ShrinkContext.Core", "com.cneicy.shrink-context-core", "0.1.0"),
|
||||
new ExpectedPackage("ShrinkContext.EventBusAdapter", "com.cneicy.shrink-context-eventbus-adapter", "0.1.0"),
|
||||
new ExpectedPackage("ShrinkDataSaver", "com.cneicy.shrink-datasaver", "2.2.0"),
|
||||
new ExpectedPackage("ShrinkDataSaver.Integration.App", "com.cneicy.shrink-datasaver-integration-app", "0.1.0"),
|
||||
new ExpectedPackage("ShrinkDataSaver.Integration.EventBus", "com.cneicy.shrink-datasaver-integration-eventbus", "2.1.0"),
|
||||
new ExpectedPackage("ShrinkEventBus", "com.cneicy.shrink-eventbus", "2.0.0"),
|
||||
new ExpectedPackage("ShrinkEventBus.Entities", "com.cneicy.shrink-eventbus-entities", "0.1.0"),
|
||||
new ExpectedPackage("ShrinkInstaller", "com.cneicy.shrink-installer", "0.1.2"),
|
||||
new ExpectedPackage("ShrinkModFramework", "com.cneicy.shrink-mod-framework", "0.2.1"),
|
||||
new ExpectedPackage("ShrinkNetwork", "com.cneicy.shrink-network", "0.2.0"),
|
||||
new ExpectedPackage("ShrinkNetwork.Integration.App", "com.cneicy.shrink-network-integration-app", "0.1.0"),
|
||||
new ExpectedPackage("ShrinkNetwork.Integration.EventBus", "com.cneicy.shrink-network-integration-eventbus", "0.1.1"),
|
||||
new ExpectedPackage("ShrinkShared.CodeGen", "com.cneicy.shrink-shared-codegen", "0.1.0"),
|
||||
new ExpectedPackage("ShrinkTutorial", "com.cneicy.shrink-tutorial", "0.1.0")
|
||||
};
|
||||
|
||||
public static void Run()
|
||||
{
|
||||
try
|
||||
{
|
||||
var workspaceRoot = Path.GetFullPath(Path.Combine(Application.dataPath, ".."));
|
||||
var packages = ReadPackages(workspaceRoot);
|
||||
ValidateGraph(packages);
|
||||
Debug.Log($"ShrinkSDK Workspace validation passed: packages={packages.Count}");
|
||||
EditorApplication.Exit(0);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.LogException(exception);
|
||||
EditorApplication.Exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private static Dictionary<string, PackageDefinition> ReadPackages(string workspaceRoot)
|
||||
{
|
||||
var result = new Dictionary<string, PackageDefinition>(StringComparer.Ordinal);
|
||||
foreach (var expected in ExpectedPackages)
|
||||
{
|
||||
var manifestPath = Path.Combine(workspaceRoot, "Assets", "Modules", expected.Directory, "package.json");
|
||||
if (!File.Exists(manifestPath))
|
||||
{
|
||||
throw new FileNotFoundException($"Required package manifest is missing: {expected.Directory}", manifestPath);
|
||||
}
|
||||
|
||||
var manifest = JObject.Parse(File.ReadAllText(manifestPath));
|
||||
var packageName = manifest.Value<string>("name");
|
||||
var version = manifest.Value<string>("version");
|
||||
if (packageName == null || version == null || packageName.Length == 0 || version.Length == 0)
|
||||
{
|
||||
throw new InvalidOperationException($"{expected.Directory} has no valid package name or version.");
|
||||
}
|
||||
|
||||
if (!string.Equals(packageName, expected.Name, StringComparison.Ordinal) ||
|
||||
!string.Equals(version, expected.Version, StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"{expected.Directory} must be {expected.Name}@{expected.Version}, found {packageName}@{version}.");
|
||||
}
|
||||
|
||||
if (result.ContainsKey(packageName))
|
||||
{
|
||||
throw new InvalidOperationException($"Duplicate package manifest name: {packageName}");
|
||||
}
|
||||
|
||||
result.Add(packageName, new PackageDefinition(packageName, version, manifest));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void ValidateGraph(IReadOnlyDictionary<string, PackageDefinition> packages)
|
||||
{
|
||||
var dependencies = new Dictionary<string, HashSet<string>>(StringComparer.Ordinal);
|
||||
foreach (var package in packages.Values)
|
||||
{
|
||||
var localDependencies = new HashSet<string>(StringComparer.Ordinal);
|
||||
var manifestDependencies = package.Manifest["dependencies"] as JObject;
|
||||
if (manifestDependencies != null)
|
||||
{
|
||||
foreach (var property in manifestDependencies.Properties())
|
||||
{
|
||||
if (!packages.TryGetValue(property.Name, out var dependency))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var expectedVersion = property.Value.Value<string>();
|
||||
if (!string.Equals(expectedVersion, dependency.Version, StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"{package.Name} requires {dependency.Name} {expectedVersion}, but the Workspace provides {dependency.Version}.");
|
||||
}
|
||||
|
||||
var isIntegrationOrStarter =
|
||||
package.Name.IndexOf("-integration-", StringComparison.Ordinal) >= 0 ||
|
||||
package.Name.IndexOf("-starter-", StringComparison.Ordinal) >= 0;
|
||||
if (dependency.Name.IndexOf("-integration-", StringComparison.Ordinal) >= 0 && !isIntegrationOrStarter)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"{package.Name} must not depend on integration package {dependency.Name}.");
|
||||
}
|
||||
|
||||
localDependencies.Add(dependency.Name);
|
||||
}
|
||||
}
|
||||
|
||||
dependencies.Add(package.Name, localDependencies);
|
||||
}
|
||||
|
||||
var resolved = new HashSet<string>(StringComparer.Ordinal);
|
||||
var madeProgress = true;
|
||||
while (madeProgress)
|
||||
{
|
||||
madeProgress = false;
|
||||
foreach (var packageName in dependencies.Keys.OrderBy(name => name, StringComparer.Ordinal))
|
||||
{
|
||||
if (resolved.Contains(packageName) || dependencies[packageName].Any(dependency => !resolved.Contains(dependency)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
resolved.Add(packageName);
|
||||
madeProgress = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (resolved.Count != packages.Count)
|
||||
{
|
||||
var blocked = dependencies
|
||||
.Where(pair => !resolved.Contains(pair.Key))
|
||||
.Select(pair => pair.Key + " -> " + string.Join(", ", pair.Value.Where(dependency => !resolved.Contains(dependency))))
|
||||
.OrderBy(value => value, StringComparer.Ordinal);
|
||||
throw new InvalidOperationException("Circular internal package dependencies detected: " + string.Join("; ", blocked));
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class PackageDefinition
|
||||
{
|
||||
public PackageDefinition(string name, string version, JObject manifest)
|
||||
{
|
||||
Name = name;
|
||||
Version = version;
|
||||
Manifest = manifest;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
public string Version { get; }
|
||||
public JObject Manifest { get; }
|
||||
}
|
||||
|
||||
private readonly struct ExpectedPackage
|
||||
{
|
||||
public ExpectedPackage(string directory, string name, string version)
|
||||
{
|
||||
Directory = directory;
|
||||
Name = name;
|
||||
Version = version;
|
||||
}
|
||||
|
||||
public string Directory { get; }
|
||||
public string Name { get; }
|
||||
public string Version { get; }
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 508c3ec513ec5194cb44d97137cf0a85
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user