380 lines
16 KiB
Bash
380 lines
16 KiB
Bash
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
workspace_root="${1:-$PWD}"
|
|
workspace_root="$(cd "$workspace_root" && pwd)"
|
|
unity_bin="${UNITY_EDITOR_PATH:-$(command -v unity-editor || command -v unity || command -v Unity || true)}"
|
|
consumer_root="$workspace_root/Temp/PublishedUpmConsumerValidation"
|
|
published_catalog="$workspace_root/Tools/UpmConsumerValidation/published-upm-versions.json"
|
|
|
|
if [[ ! -f "$workspace_root/ProjectSettings/ProjectVersion.txt" ]]; then
|
|
echo "Workspace ProjectVersion.txt was not found: $workspace_root" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ -z "$unity_bin" ]]; then
|
|
echo "Unity editor executable was not found. Set UNITY_EDITOR_PATH." >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ ! -f "$published_catalog" ]]; then
|
|
echo "Published package catalog was not found: $published_catalog" >&2
|
|
exit 1
|
|
fi
|
|
|
|
published_value() {
|
|
local package_name="$1"
|
|
local property_name="$2"
|
|
node --input-type=module - "$published_catalog" "$package_name" "$property_name" <<'NODE'
|
|
import { readFileSync } from 'node:fs';
|
|
|
|
const [, , catalogPath, packageName, propertyName] = process.argv;
|
|
const catalog = JSON.parse(readFileSync(catalogPath, 'utf8'));
|
|
const value = catalog.packages?.[packageName]?.[propertyName];
|
|
if (typeof value !== 'string' || value.length === 0) {
|
|
throw new Error(`Published catalog value is missing: ${packageName}.${propertyName}`);
|
|
}
|
|
if (propertyName === 'version' && !/^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/.test(value)) {
|
|
throw new Error(`Published catalog version is invalid: ${packageName}=${value}`);
|
|
}
|
|
process.stdout.write(value);
|
|
NODE
|
|
}
|
|
|
|
export SHRINKSDK_PUBLISHED_STARTER_VERSION="$(published_value com.cneicy.shrink-app-starter-basic version)"
|
|
export SHRINKSDK_PUBLISHED_CONTEXT_APP_VERSION="$(published_value com.cneicy.shrink-context-app-adapter version)"
|
|
export SHRINKSDK_PUBLISHED_CONTEXT_EVENTBUS_VERSION="$(published_value com.cneicy.shrink-context-eventbus-adapter version)"
|
|
export SHRINKSDK_PUBLISHED_MOD_VERSION="$(published_value com.cneicy.shrink-mod-framework version)"
|
|
export SHRINKSDK_PUBLISHED_TUTORIAL_VERSION="$(published_value com.cneicy.shrink-tutorial version)"
|
|
export SHRINKSDK_PUBLISHED_INSTALLER_VERSION="$(published_value com.cneicy.shrink-installer version)"
|
|
export SHRINKSDK_PUBLISHED_MOD_REPOSITORY="$(published_value com.cneicy.shrink-mod-framework repository)"
|
|
export SHRINKSDK_PUBLISHED_INSTALLER_REPOSITORY="$(published_value com.cneicy.shrink-installer repository)"
|
|
|
|
case "$consumer_root" in
|
|
"$workspace_root"/Temp/*) ;;
|
|
*) echo "Consumer project must stay under the workspace Temp directory." >&2; exit 1 ;;
|
|
esac
|
|
|
|
write_validator() {
|
|
local project_root="$1"
|
|
cat > "$project_root/Assets/Editor/PublishedUpmConsumerValidator.cs" <<'EOF'
|
|
#if UNITY_EDITOR
|
|
#nullable enable
|
|
|
|
using System;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using Newtonsoft.Json.Linq;
|
|
using UnityEditor;
|
|
using UnityEditor.Compilation;
|
|
using UnityEditor.PackageManager;
|
|
using UnityEngine;
|
|
using PackageInfo = UnityEditor.PackageManager.PackageInfo;
|
|
|
|
public static class PublishedUpmConsumerValidator
|
|
{
|
|
private const string RegistryUrl = "https://git.crash.work/api/packages/ShrinkSDK/npm/";
|
|
private const string UniTaskUrl = "https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask#7c0f199fe0d3fc528024488ccd671e6c7b27745b";
|
|
private static readonly string StarterVersion = RequiredEnvironment("SHRINKSDK_PUBLISHED_STARTER_VERSION");
|
|
private static readonly string ContextAppVersion = RequiredEnvironment("SHRINKSDK_PUBLISHED_CONTEXT_APP_VERSION");
|
|
private static readonly string ContextEventBusVersion = RequiredEnvironment("SHRINKSDK_PUBLISHED_CONTEXT_EVENTBUS_VERSION");
|
|
private static readonly string ModVersion = RequiredEnvironment("SHRINKSDK_PUBLISHED_MOD_VERSION");
|
|
private static readonly string TutorialVersion = RequiredEnvironment("SHRINKSDK_PUBLISHED_TUTORIAL_VERSION");
|
|
private static readonly string InstallerVersion = RequiredEnvironment("SHRINKSDK_PUBLISHED_INSTALLER_VERSION");
|
|
private static readonly string ModRepository = RequiredEnvironment("SHRINKSDK_PUBLISHED_MOD_REPOSITORY");
|
|
private static readonly string InstallerRepository = RequiredEnvironment("SHRINKSDK_PUBLISHED_INSTALLER_REPOSITORY");
|
|
|
|
public static void VerifyRegistry()
|
|
{
|
|
Run(() =>
|
|
{
|
|
VerifyPackage("com.cneicy.shrink-app-starter-basic", StarterVersion, "ShrinkApp.Starter.Basic.Runtime");
|
|
VerifyPackage("com.cneicy.shrink-context-app-adapter", ContextAppVersion, "ShrinkContext.AppAdapter.Runtime");
|
|
VerifyPackage("com.cneicy.shrink-context-eventbus-adapter", ContextEventBusVersion, "ShrinkContext.EventBusAdapter.Runtime");
|
|
VerifyPackage("com.cneicy.shrink-mod-framework", ModVersion, "ShrinkModFramework.Runtime");
|
|
VerifyPackage("com.cneicy.shrink-tutorial", TutorialVersion, "ShrinkTutorial.Runtime");
|
|
RequireManifestValue("com.cneicy.shrink-app-starter-basic", StarterVersion);
|
|
RequireManifestValue("com.cneicy.shrink-context-eventbus-adapter", ContextEventBusVersion);
|
|
RequireManifestValue("com.cneicy.shrink-mod-framework", ModVersion);
|
|
RequireManifestValue("com.cneicy.shrink-tutorial", TutorialVersion);
|
|
RequireManifestValue("com.cysharp.unitask", UniTaskUrl);
|
|
RequireRegistry("com.cneicy", RegistryUrl);
|
|
});
|
|
}
|
|
|
|
public static void VerifyGitTag()
|
|
{
|
|
Run(() =>
|
|
{
|
|
VerifyPackage("com.cneicy.shrink-mod-framework", ModVersion, "ShrinkModFramework.Runtime");
|
|
RequireManifestValue(
|
|
"com.cneicy.shrink-mod-framework",
|
|
ModRepository + "#v" + ModVersion);
|
|
RequireManifestValue("com.cysharp.unitask", UniTaskUrl);
|
|
RequireRegistry("com.cneicy", RegistryUrl);
|
|
});
|
|
}
|
|
|
|
public static void ApplyInstaller()
|
|
{
|
|
try
|
|
{
|
|
VerifyPackage("com.cneicy.shrink-installer", InstallerVersion, "ShrinkInstaller.Editor");
|
|
RequireManifestValue(
|
|
"com.cneicy.shrink-installer",
|
|
InstallerRepository + "#v" + InstallerVersion);
|
|
|
|
var catalog = FindType("ShrinkSDK.Installer.ShrinkSdkPackageCatalog");
|
|
var starter = catalog.GetField("StarterBasic", BindingFlags.Static | BindingFlags.NonPublic)
|
|
?.GetValue(null) ?? throw new InvalidOperationException("Installer StarterBasic catalog entry was not found.");
|
|
var manifestStore = FindType("ShrinkSDK.Installer.ShrinkSdkManifestStore");
|
|
var install = manifestStore.GetMethod(
|
|
"EnsurePackageInstallation",
|
|
BindingFlags.Static | BindingFlags.NonPublic)
|
|
?? throw new InvalidOperationException("Installer manifest installation method was not found.");
|
|
var roots = Array.CreateInstance(starter.GetType(), 1);
|
|
roots.SetValue(starter, 0);
|
|
install.Invoke(null, new object[] { roots });
|
|
|
|
RequireManifestValue("com.cneicy.shrink-app-starter-basic", StarterVersion);
|
|
RequireManifestValue("com.cysharp.unitask", UniTaskUrl);
|
|
RequireRegistry("com.cneicy", RegistryUrl);
|
|
RequireRegistry("com.example", "https://registry.npmjs.org/");
|
|
Succeed("Installer wrote the fixed Starter Basic manifest entry.");
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
Fail(Unwrap(exception));
|
|
}
|
|
}
|
|
|
|
public static void VerifyInstallerResult()
|
|
{
|
|
Run(() =>
|
|
{
|
|
VerifyPackage("com.cneicy.shrink-installer", InstallerVersion, "ShrinkInstaller.Editor");
|
|
VerifyPackage("com.cneicy.shrink-app-starter-basic", StarterVersion, "ShrinkApp.Starter.Basic.Runtime");
|
|
VerifyPackage("com.cneicy.shrink-context-app-adapter", ContextAppVersion, "ShrinkContext.AppAdapter.Runtime");
|
|
RequireManifestValue("com.cneicy.shrink-app-starter-basic", StarterVersion);
|
|
RequireManifestValue("com.cysharp.unitask", UniTaskUrl);
|
|
RequireRegistry("com.cneicy", RegistryUrl);
|
|
RequireRegistry("com.example", "https://registry.npmjs.org/");
|
|
});
|
|
}
|
|
|
|
private static void Run(Action action)
|
|
{
|
|
try
|
|
{
|
|
action();
|
|
Succeed("Published UPM consumer validation passed.");
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
Fail(Unwrap(exception));
|
|
}
|
|
}
|
|
|
|
private static string RequiredEnvironment(string name)
|
|
{
|
|
return Environment.GetEnvironmentVariable(name)
|
|
?? throw new InvalidOperationException("Required environment variable is missing: " + name);
|
|
}
|
|
|
|
private static void VerifyPackage(string packageName, string version, string assemblyName)
|
|
{
|
|
var package = PackageInfo.GetAllRegisteredPackages()
|
|
.FirstOrDefault(candidate => string.Equals(candidate.name, packageName, StringComparison.Ordinal));
|
|
if (package == null)
|
|
{
|
|
throw new InvalidOperationException("Package was not resolved: " + packageName);
|
|
}
|
|
|
|
if (!string.Equals(package.version, version, StringComparison.Ordinal))
|
|
{
|
|
throw new InvalidOperationException(
|
|
"Package version mismatch for " + packageName + ": " + package.version + " != " + version);
|
|
}
|
|
|
|
if (!CompilationPipeline.GetAssemblies().Any(assembly =>
|
|
string.Equals(assembly.name, assemblyName, StringComparison.Ordinal)))
|
|
{
|
|
throw new InvalidOperationException("Expected assembly was not compiled: " + assemblyName);
|
|
}
|
|
}
|
|
|
|
private static void RequireManifestValue(string packageName, string expectedValue)
|
|
{
|
|
var manifest = ReadManifest();
|
|
var actualValue = (string?)manifest["dependencies"]?[packageName];
|
|
if (!string.Equals(actualValue, expectedValue, StringComparison.Ordinal))
|
|
{
|
|
throw new InvalidOperationException(
|
|
"Manifest dependency mismatch for " + packageName + ": " + actualValue + " != " + expectedValue);
|
|
}
|
|
}
|
|
|
|
private static void RequireRegistry(string scope, string url)
|
|
{
|
|
var registries = ReadManifest()["scopedRegistries"] as JArray;
|
|
var found = registries?.OfType<JObject>().Any(registry =>
|
|
string.Equals(NormalizeUrl((string?)registry["url"]), NormalizeUrl(url), StringComparison.OrdinalIgnoreCase) &&
|
|
(registry["scopes"] as JArray)?.Values<string>().Any(value =>
|
|
string.Equals(value, scope, StringComparison.Ordinal)) == true) == true;
|
|
if (!found)
|
|
{
|
|
throw new InvalidOperationException("Scoped registry was not preserved or added: " + scope + " -> " + url);
|
|
}
|
|
}
|
|
|
|
private static JObject ReadManifest()
|
|
{
|
|
var manifestPath = Path.Combine(Path.GetFullPath(Path.Combine(Application.dataPath, "..")), "Packages", "manifest.json");
|
|
return JObject.Parse(File.ReadAllText(manifestPath));
|
|
}
|
|
|
|
private static Type FindType(string fullName)
|
|
{
|
|
return AppDomain.CurrentDomain.GetAssemblies()
|
|
.Select(assembly => assembly.GetType(fullName, false))
|
|
.FirstOrDefault(type => type != null)
|
|
?? throw new InvalidOperationException("Installer type was not loaded: " + fullName);
|
|
}
|
|
|
|
private static string NormalizeUrl(string? url) => (url ?? string.Empty).Trim().TrimEnd('/') + "/";
|
|
|
|
private static Exception Unwrap(Exception exception) =>
|
|
exception is TargetInvocationException { InnerException: not null } ? exception.InnerException : exception;
|
|
|
|
private static void Succeed(string message)
|
|
{
|
|
Debug.Log(message);
|
|
EditorApplication.Exit(0);
|
|
}
|
|
|
|
private static void Fail(Exception exception)
|
|
{
|
|
Debug.LogException(exception);
|
|
EditorApplication.Exit(1);
|
|
}
|
|
}
|
|
#endif
|
|
EOF
|
|
}
|
|
|
|
prepare_consumer() {
|
|
local name="$1"
|
|
local project_root="$consumer_root/$name"
|
|
mkdir -p "$project_root/Packages" "$project_root/Assets/Editor" "$project_root/ProjectSettings"
|
|
cp "$workspace_root/ProjectSettings/ProjectVersion.txt" "$project_root/ProjectSettings/ProjectVersion.txt"
|
|
write_validator "$project_root"
|
|
printf '%s' "$project_root"
|
|
}
|
|
|
|
write_manifest() {
|
|
local project_root="$1"
|
|
local manifest="$2"
|
|
printf '%s\n' "$manifest" > "$project_root/Packages/manifest.json"
|
|
}
|
|
|
|
show_failure() {
|
|
local log_path="$1"
|
|
echo "Unity consumer validation failed. Relevant log lines:" >&2
|
|
grep -in -E 'exception|buildfailed|build failed|error|failed|compilererror|invalidoperation|nullreference|package manager' "$log_path" | tail -n 180 >&2 || true
|
|
}
|
|
|
|
run_unity() {
|
|
local project_root="$1"
|
|
local method="$2"
|
|
local log_path="$3"
|
|
local wait_for_resolve="$4"
|
|
local args=(
|
|
-batchmode
|
|
-nographics
|
|
-projectPath "$project_root"
|
|
-executeMethod "$method"
|
|
-logFile "$log_path"
|
|
)
|
|
if [[ "$wait_for_resolve" != "true" ]]; then
|
|
args+=(-quit)
|
|
fi
|
|
|
|
if ! "$unity_bin" "${args[@]}"; then
|
|
show_failure "$log_path"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
rm -rf "$consumer_root"
|
|
mkdir -p "$consumer_root"
|
|
|
|
registry_project="$(prepare_consumer registry)"
|
|
registry_manifest='{
|
|
"scopedRegistries": [
|
|
{
|
|
"name": "ShrinkSDK",
|
|
"url": "https://git.crash.work/api/packages/ShrinkSDK/npm/",
|
|
"scopes": ["com.cneicy"]
|
|
}
|
|
],
|
|
"dependencies": {
|
|
"com.cneicy.shrink-app-starter-basic": "__STARTER_VERSION__",
|
|
"com.cneicy.shrink-context-eventbus-adapter": "__CONTEXT_EVENTBUS_VERSION__",
|
|
"com.cneicy.shrink-mod-framework": "__MOD_VERSION__",
|
|
"com.cneicy.shrink-tutorial": "__TUTORIAL_VERSION__",
|
|
"com.cysharp.unitask": "https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask#7c0f199fe0d3fc528024488ccd671e6c7b27745b",
|
|
"com.unity.nuget.newtonsoft-json": "3.2.2"
|
|
}
|
|
}'
|
|
registry_manifest="${registry_manifest//__STARTER_VERSION__/$SHRINKSDK_PUBLISHED_STARTER_VERSION}"
|
|
registry_manifest="${registry_manifest//__CONTEXT_EVENTBUS_VERSION__/$SHRINKSDK_PUBLISHED_CONTEXT_EVENTBUS_VERSION}"
|
|
registry_manifest="${registry_manifest//__MOD_VERSION__/$SHRINKSDK_PUBLISHED_MOD_VERSION}"
|
|
registry_manifest="${registry_manifest//__TUTORIAL_VERSION__/$SHRINKSDK_PUBLISHED_TUTORIAL_VERSION}"
|
|
write_manifest "$registry_project" "$registry_manifest"
|
|
run_unity "$registry_project" PublishedUpmConsumerValidator.VerifyRegistry "$registry_project/registry.log" false
|
|
|
|
git_project="$(prepare_consumer git-tag)"
|
|
git_manifest='{
|
|
"scopedRegistries": [
|
|
{
|
|
"name": "ShrinkSDK",
|
|
"url": "https://git.crash.work/api/packages/ShrinkSDK/npm/",
|
|
"scopes": ["com.cneicy"]
|
|
}
|
|
],
|
|
"dependencies": {
|
|
"com.cneicy.shrink-mod-framework": "__MOD_REPOSITORY__#v__MOD_VERSION__",
|
|
"com.cysharp.unitask": "https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask#7c0f199fe0d3fc528024488ccd671e6c7b27745b",
|
|
"com.unity.nuget.newtonsoft-json": "3.2.2"
|
|
}
|
|
}'
|
|
git_manifest="${git_manifest//__MOD_REPOSITORY__/$SHRINKSDK_PUBLISHED_MOD_REPOSITORY}"
|
|
git_manifest="${git_manifest//__MOD_VERSION__/$SHRINKSDK_PUBLISHED_MOD_VERSION}"
|
|
write_manifest "$git_project" "$git_manifest"
|
|
run_unity "$git_project" PublishedUpmConsumerValidator.VerifyGitTag "$git_project/git-tag.log" false
|
|
|
|
installer_project="$(prepare_consumer installer)"
|
|
installer_manifest='{
|
|
"scopedRegistries": [
|
|
{
|
|
"name": "Existing Registry",
|
|
"url": "https://registry.npmjs.org/",
|
|
"scopes": ["com.example"]
|
|
}
|
|
],
|
|
"dependencies": {
|
|
"com.cneicy.shrink-installer": "__INSTALLER_REPOSITORY__#v__INSTALLER_VERSION__",
|
|
"com.unity.nuget.newtonsoft-json": "3.2.2",
|
|
"com.unity.test-framework": "1.1.33"
|
|
}
|
|
}'
|
|
installer_manifest="${installer_manifest//__INSTALLER_REPOSITORY__/$SHRINKSDK_PUBLISHED_INSTALLER_REPOSITORY}"
|
|
installer_manifest="${installer_manifest//__INSTALLER_VERSION__/$SHRINKSDK_PUBLISHED_INSTALLER_VERSION}"
|
|
write_manifest "$installer_project" "$installer_manifest"
|
|
run_unity "$installer_project" PublishedUpmConsumerValidator.ApplyInstaller "$installer_project/installer-apply.log" false
|
|
run_unity "$installer_project" PublishedUpmConsumerValidator.VerifyInstallerResult "$installer_project/installer-result.log" false
|
|
|
|
rm -rf "$consumer_root"
|
|
echo "Published UPM consumer validation passed: registry, exact Git tag, installer."
|