#!/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" local value value="$(awk -v package_name="$package_name" -v property_name="$property_name" ' index($0, "\"" package_name "\"") { in_package = 1 next } in_package && index($0, "\"" property_name "\"") { value = $0 sub(/^[^:]*:[[:space:]]*"/, "", value) sub(/".*$/, "", value) print value found = 1 exit } in_package && /^[[:space:]]*}/ { exit } END { if (!found) exit 1 } ' "$published_catalog")" || { echo "Published catalog value is missing: ${package_name}.${property_name}" >&2 return 1 } if [[ -z "$value" ]]; then echo "Published catalog value is empty: ${package_name}.${property_name}" >&2 return 1 fi if [[ "$property_name" == "version" && ! "$value" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then echo "Published catalog version is invalid: ${package_name}=${value}" >&2 return 1 fi printf '%s' "$value" } 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().Any(registry => string.Equals(NormalizeUrl((string?)registry["url"]), NormalizeUrl(url), StringComparison.OrdinalIgnoreCase) && (registry["scopes"] as JArray)?.Values().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."