commit c3b57fd0239425eaf09afa5e9a77c2f2c439aa61 Author: cneicy Date: Wed Aug 26 02:50:28 2026 +0800 chore: initialize standalone UPM package diff --git a/.gitea/workflows/publish.yml b/.gitea/workflows/publish.yml new file mode 100644 index 0000000..ff9d651 --- /dev/null +++ b/.gitea/workflows/publish.yml @@ -0,0 +1,49 @@ +name: Publish UPM package + +on: + push: + tags: + - 'v*' + workflow_dispatch: + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + env: + NODE_AUTH_TOKEN: ${{ secrets.SHRINKSDK_PACKAGE_TOKEN }} + steps: + - name: Fetch tagged revision + shell: bash + run: | + set -eu + ref="${{ gitea.sha }}" + test -n "$ref" + git init . + git remote add origin "https://git.crash.work/ShrinkSDK/Installer.git" + git fetch --depth=1 origin "$ref" + git checkout --detach FETCH_HEAD + + - name: Validate immutable release version + shell: bash + run: | + set -eu + tag="$(git describe --exact-match --tags HEAD)" + version="$(node -p "require('./package.json').version")" + test "$tag" = "v$version" + npm pack --dry-run + + - name: Publish to ShrinkSDK registry + shell: bash + run: | + set -eu + : "${NODE_AUTH_TOKEN:?SHRINKSDK_PACKAGE_TOKEN is required}" + npmrc="$HOME/.npmrc" + cleanup() { rm -f "$npmrc"; } + trap cleanup EXIT + printf '%s\n' \ + 'registry=https://git.crash.work/api/packages/ShrinkSDK/npm/' \ + '//git.crash.work/api/packages/ShrinkSDK/npm/:_authToken=${NODE_AUTH_TOKEN}' > "$npmrc" + npm publish --registry=https://git.crash.work/api/packages/ShrinkSDK/npm/ \ No newline at end of file diff --git a/.gitea/workflows/unity-verify.yml b/.gitea/workflows/unity-verify.yml new file mode 100644 index 0000000..c604475 --- /dev/null +++ b/.gitea/workflows/unity-verify.yml @@ -0,0 +1,39 @@ +name: Verify standalone Unity package + +on: + workflow_dispatch: + +jobs: + editmode: + runs-on: unity-2022.3.62f3 + container: + image: docker.1panel.live/unityci/editor:ubuntu-2022.3.62f3-windows-mono-3 + volumes: + - /www/dk_project/bt-recovery/gitea/runner/seedskey-unity-license:/root/.local/share/unity3d/Unity + - /www/dk_project/bt-recovery/gitea/runner/seedskey-unity-entitlements:/root/.config/unity3d/Unity/licenses + steps: + - name: Fetch selected revision + shell: bash + run: | + set -eu + ref="${{ gitea.sha }}" + git init . + git remote add origin "https://git.crash.work/ShrinkSDK/Installer.git" + git fetch --depth=1 origin "$ref" + git checkout --detach FETCH_HEAD + + - name: Run package EditMode tests + shell: bash + run: | + set -eu + unity_bin="$(command -v unity-editor || command -v unity || command -v Unity || true)" + test -n "$unity_bin" + "$unity_bin" \ + -batchmode \ + -nographics \ + -quit \ + -projectPath "$PWD/Development~/UnityProject" \ + -runTests \ + -testPlatform EditMode \ + -testResults "$PWD/TestResults/editmode.xml" \ + -logFile "$PWD/TestResults/unity.log" \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..246935d --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +/Development~/UnityProject/[Ll]ibrary/ +/Development~/UnityProject/[Tt]emp/ +/Development~/UnityProject/[Oo]bj/ +/Development~/UnityProject/[Ll]ogs/ +/Development~/UnityProject/[Uu]ser[Ss]ettings/ +/Development~/UnityProject/TestResults/ +/Tools~/**/[Bb]in/ +/Tools~/**/[Oo]bj/ +*.user +*.DotSettings.user diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..700f03d --- /dev/null +++ b/.npmignore @@ -0,0 +1,8 @@ +.git/ +.gitea/ +Development~/ +Tools~/ +*.csproj +*.sln +*.user +*.DotSettings.user diff --git a/Development~/UnityProject/.gitignore b/Development~/UnityProject/.gitignore new file mode 100644 index 0000000..aa2eb00 --- /dev/null +++ b/Development~/UnityProject/.gitignore @@ -0,0 +1,6 @@ +[Ll]ibrary/ +[Tt]emp/ +[Oo]bj/ +[Ll]ogs/ +[Uu]ser[Ss]ettings/ +TestResults/ \ No newline at end of file diff --git a/Development~/UnityProject/Assets/.gitkeep b/Development~/UnityProject/Assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/Development~/UnityProject/Packages/manifest.json b/Development~/UnityProject/Packages/manifest.json new file mode 100644 index 0000000..8ad7bdf --- /dev/null +++ b/Development~/UnityProject/Packages/manifest.json @@ -0,0 +1,15 @@ +{ + "scopedRegistries": [ + { + "name": "ShrinkSDK", + "url": "https://git.crash.work/api/packages/ShrinkSDK/npm/", + "scopes": [ + "com.cneicy" + ] + } + ], + "dependencies": { + "com.unity.test-framework": "1.1.33", + "com.cneicy.shrink-installer": "file:../../.." + } +} diff --git a/Development~/UnityProject/ProjectSettings/ProjectVersion.txt b/Development~/UnityProject/ProjectSettings/ProjectVersion.txt new file mode 100644 index 0000000..587f809 --- /dev/null +++ b/Development~/UnityProject/ProjectSettings/ProjectVersion.txt @@ -0,0 +1,2 @@ +m_EditorVersion: 2022.3.62f3 +m_EditorVersionWithRevision: 2022.3.62f3 (96770f904ca7) diff --git a/Editor/ShrinkInstaller.Editor.asmdef b/Editor/ShrinkInstaller.Editor.asmdef new file mode 100644 index 0000000..73c5f01 --- /dev/null +++ b/Editor/ShrinkInstaller.Editor.asmdef @@ -0,0 +1,17 @@ +{ + "name": "ShrinkInstaller.Editor", + "rootNamespace": "ShrinkSDK.Installer", + "references": [ + "Unity.Newtonsoft.Json" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/Editor/ShrinkSdkInstallerWindow.cs b/Editor/ShrinkSdkInstallerWindow.cs new file mode 100644 index 0000000..ee2d608 --- /dev/null +++ b/Editor/ShrinkSdkInstallerWindow.cs @@ -0,0 +1,175 @@ +#if UNITY_EDITOR +#nullable enable + +using System; +using System.Linq; +using UnityEditor; +using UnityEditor.PackageManager; +using UnityEditor.PackageManager.Requests; +using UnityEngine; + +namespace ShrinkSDK.Installer +{ + internal sealed class ShrinkSdkInstallerWindow : EditorWindow + { + private AddRequest? _addRequest; + private ListRequest? _listRequest; + private ResolveRequest? _resolveRequest; + private ShrinkSdkPackageEntry? _pendingInstall; + private int _selectedPackageIndex; + private string _status = "Ready"; + private MessageType _statusType = MessageType.Info; + + [MenuItem("ShrinkSDK/Packages")] + private static void Open() + { + GetWindow("ShrinkSDK Packages"); + } + + private void OnEnable() + { + RefreshDiagnostics(); + } + + private void OnGUI() + { + EditorGUILayout.LabelField("ShrinkSDK", EditorStyles.boldLabel); + EditorGUILayout.Space(4f); + EditorGUILayout.LabelField("Registry", ShrinkSdkManifestStore.HasShrinkSdkRegistry() + ? ShrinkSdkPackageCatalog.RegistryUrl + : "Not configured"); + + using (new EditorGUI.DisabledScope(IsBusy)) + { + if (GUILayout.Button("Configure Registry")) + { + ConfigureRegistry(); + } + + EditorGUILayout.Space(8f); + if (GUILayout.Button("Install Starter Basic")) + { + BeginInstall(ShrinkSdkPackageCatalog.StarterBasic); + } + + var labels = ShrinkSdkPackageCatalog.Packages.Select(entry => + entry.DisplayName + " " + entry.Version).ToArray(); + _selectedPackageIndex = EditorGUILayout.Popup("Package", _selectedPackageIndex, labels); + if (GUILayout.Button("Install Selected Package")) + { + BeginInstall(ShrinkSdkPackageCatalog.Packages[_selectedPackageIndex]); + } + + if (GUILayout.Button("Refresh Diagnostics")) + { + RefreshDiagnostics(); + } + } + + EditorGUILayout.Space(8f); + EditorGUILayout.HelpBox(_status, _statusType); + } + + private bool IsBusy => _resolveRequest != null || _addRequest != null || _listRequest != null; + + private void Update() + { + if (_resolveRequest != null && _resolveRequest.IsCompleted) + { + var request = _resolveRequest; + _resolveRequest = null; + if (request.Status != StatusCode.Success) + { + SetStatus("Registry resolve failed: " + request.Error.message, MessageType.Error); + return; + } + + if (_pendingInstall != null) + { + _addRequest = Client.Add(_pendingInstall.PackageSpecifier); + SetStatus("Installing " + _pendingInstall.PackageSpecifier, MessageType.Info); + } + } + + if (_addRequest != null && _addRequest.IsCompleted) + { + var request = _addRequest; + _addRequest = null; + if (request.Status == StatusCode.Success) + { + SetStatus("Installed " + request.Result.name + " " + request.Result.version, MessageType.Info); + } + else + { + SetStatus("Package install failed: " + request.Error.message, MessageType.Error); + } + + _pendingInstall = null; + } + + if (_listRequest != null && _listRequest.IsCompleted) + { + var request = _listRequest; + _listRequest = null; + if (request.Status == StatusCode.Success) + { + var installed = request.Result.Count(package => package.name.StartsWith("com.cneicy.", StringComparison.Ordinal)); + SetStatus("Registry is configured. Installed ShrinkSDK packages: " + installed, MessageType.Info); + } + else + { + SetStatus("Package diagnostics failed: " + request.Error.message, MessageType.Warning); + } + } + } + + private void ConfigureRegistry() + { + try + { + ShrinkSdkManifestStore.EnsureShrinkSdkRegistry(); + _pendingInstall = null; + _resolveRequest = Client.Resolve(); + SetStatus("Registry configuration written. Resolving packages.", MessageType.Info); + } + catch (Exception exception) + { + SetStatus(exception.Message, MessageType.Error); + } + } + + private void BeginInstall(ShrinkSdkPackageEntry entry) + { + try + { + ShrinkSdkManifestStore.EnsureShrinkSdkRegistry(); + _pendingInstall = entry; + _resolveRequest = Client.Resolve(); + SetStatus("Resolving registry for " + entry.PackageSpecifier, MessageType.Info); + } + catch (Exception exception) + { + SetStatus(exception.Message, MessageType.Error); + } + } + + private void RefreshDiagnostics() + { + if (IsBusy) + { + return; + } + + _listRequest = Client.List(true); + SetStatus("Refreshing installed package diagnostics.", MessageType.Info); + } + + private void SetStatus(string message, MessageType messageType) + { + _status = message; + _statusType = messageType; + Repaint(); + } + } +} +#endif diff --git a/Editor/ShrinkSdkManifestStore.cs b/Editor/ShrinkSdkManifestStore.cs new file mode 100644 index 0000000..5b4e5da --- /dev/null +++ b/Editor/ShrinkSdkManifestStore.cs @@ -0,0 +1,131 @@ +#if UNITY_EDITOR +#nullable enable + +using System; +using System.IO; +using System.Linq; +using System.Text; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using UnityEngine; + +namespace ShrinkSDK.Installer +{ + internal static class ShrinkSdkManifestStore + { + internal static string ManifestPath => Path.Combine( + Path.GetFullPath(Path.Combine(Application.dataPath, "..")), + "Packages", + "manifest.json"); + + internal static bool HasShrinkSdkRegistry() + { + var manifest = ReadManifest(); + return GetShrinkSdkRegistry(manifest) != null; + } + + internal static void EnsureShrinkSdkRegistry() + { + var manifest = ReadManifest(); + var registries = manifest["scopedRegistries"] as JArray; + if (registries == null) + { + registries = new JArray(); + manifest["scopedRegistries"] = registries; + } + + var conflictingRegistry = registries + .OfType() + .FirstOrDefault(registry => + !UrlEquals((string?)registry["url"], ShrinkSdkPackageCatalog.RegistryUrl) && + (registry["scopes"] as JArray)?.Values().Any(scope => + string.Equals(scope, ShrinkSdkPackageCatalog.RegistryScope, StringComparison.Ordinal)) == true); + + if (conflictingRegistry != null) + { + throw new InvalidOperationException( + "Packages/manifest.json already maps com.cneicy to another scoped registry. Resolve that registry explicitly before installing ShrinkSDK."); + } + + var registry = GetShrinkSdkRegistry(manifest); + if (registry == null) + { + registry = new JObject + { + ["name"] = ShrinkSdkPackageCatalog.RegistryName, + ["url"] = ShrinkSdkPackageCatalog.RegistryUrl, + ["scopes"] = new JArray(ShrinkSdkPackageCatalog.RegistryScope) + }; + registries.Add(registry); + } + else + { + registry["name"] = ShrinkSdkPackageCatalog.RegistryName; + registry["url"] = ShrinkSdkPackageCatalog.RegistryUrl; + var scopes = registry["scopes"] as JArray ?? new JArray(); + if (!scopes.Values().Any(scope => + string.Equals(scope, ShrinkSdkPackageCatalog.RegistryScope, StringComparison.Ordinal))) + { + scopes.Add(ShrinkSdkPackageCatalog.RegistryScope); + } + + registry["scopes"] = scopes; + } + + AtomicWrite(manifest.ToString(Formatting.Indented) + Environment.NewLine); + } + + private static JObject ReadManifest() + { + if (!File.Exists(ManifestPath)) + { + throw new FileNotFoundException("Unity package manifest was not found.", ManifestPath); + } + + return JObject.Parse(File.ReadAllText(ManifestPath, Encoding.UTF8)); + } + + private static JObject? GetShrinkSdkRegistry(JObject manifest) + { + return (manifest["scopedRegistries"] as JArray)? + .OfType() + .FirstOrDefault(registry => UrlEquals((string?)registry["url"], ShrinkSdkPackageCatalog.RegistryUrl)); + } + + private static bool UrlEquals(string? left, string right) + { + return string.Equals(NormalizeUrl(left), NormalizeUrl(right), StringComparison.OrdinalIgnoreCase); + } + + private static string NormalizeUrl(string? url) + { + return (url ?? string.Empty).Trim().TrimEnd('/') + "/"; + } + + private static void AtomicWrite(string content) + { + var directory = Path.GetDirectoryName(ManifestPath); + if (string.IsNullOrWhiteSpace(directory)) + { + throw new InvalidOperationException("Unity package manifest directory could not be resolved."); + } + + var temporaryPath = Path.Combine(directory, ".manifest.shrinksdk.tmp"); + File.WriteAllText(temporaryPath, content, new UTF8Encoding(false)); + try + { + File.Replace(temporaryPath, ManifestPath, null); + } + catch + { + if (File.Exists(temporaryPath)) + { + File.Delete(temporaryPath); + } + + throw; + } + } + } +} +#endif diff --git a/Editor/ShrinkSdkPackageCatalog.cs b/Editor/ShrinkSdkPackageCatalog.cs new file mode 100644 index 0000000..5dc496d --- /dev/null +++ b/Editor/ShrinkSdkPackageCatalog.cs @@ -0,0 +1,59 @@ +#if UNITY_EDITOR +#nullable enable + +using System.Collections.Generic; + +namespace ShrinkSDK.Installer +{ + internal sealed class ShrinkSdkPackageEntry + { + public ShrinkSdkPackageEntry(string displayName, string packageName, string version) + { + DisplayName = displayName; + PackageName = packageName; + Version = version; + } + + public string DisplayName { get; } + public string PackageName { get; } + public string Version { get; } + public string PackageSpecifier => PackageName + "@" + Version; + } + + internal static class ShrinkSdkPackageCatalog + { + internal const string RegistryName = "ShrinkSDK"; + internal const string RegistryUrl = "https://git.crash.work/api/packages/ShrinkSDK/npm/"; + internal const string RegistryScope = "com.cneicy"; + + internal static readonly ShrinkSdkPackageEntry StarterBasic = new( + "ShrinkApp Starter Basic", + "com.cneicy.shrink-app-starter-basic", + "0.1.0"); + + internal static readonly IReadOnlyList Packages = new[] + { + new ShrinkSdkPackageEntry("ShrinkApp Core", "com.cneicy.shrink-app-core", "0.1.1"), + StarterBasic, + new ShrinkSdkPackageEntry("ShrinkCommand", "com.cneicy.shrink-command", "0.2.0"), + new ShrinkSdkPackageEntry("ShrinkCommand Integration App", "com.cneicy.shrink-command-integration-app", "0.1.0"), + new ShrinkSdkPackageEntry("ShrinkCommand Integration EventBus", "com.cneicy.shrink-command-integration-eventbus", "0.1.1"), + new ShrinkSdkPackageEntry("ShrinkCommand Integration Network", "com.cneicy.shrink-command-integration-network", "0.1.0"), + new ShrinkSdkPackageEntry("ShrinkContext App Adapter", "com.cneicy.shrink-context-app-adapter", "0.1.0"), + new ShrinkSdkPackageEntry("ShrinkContext Core", "com.cneicy.shrink-context-core", "0.1.0"), + new ShrinkSdkPackageEntry("ShrinkContext EventBus Adapter", "com.cneicy.shrink-context-eventbus-adapter", "0.1.0"), + new ShrinkSdkPackageEntry("ShrinkDataSaver", "com.cneicy.shrink-datasaver", "2.2.0"), + new ShrinkSdkPackageEntry("ShrinkDataSaver Integration App", "com.cneicy.shrink-datasaver-integration-app", "0.1.0"), + new ShrinkSdkPackageEntry("ShrinkDataSaver Integration EventBus", "com.cneicy.shrink-datasaver-integration-eventbus", "2.1.0"), + new ShrinkSdkPackageEntry("ShrinkEventBus", "com.cneicy.shrink-eventbus", "2.0.0"), + new ShrinkSdkPackageEntry("ShrinkEventBus Entities", "com.cneicy.shrink-eventbus-entities", "0.1.0"), + new ShrinkSdkPackageEntry("ShrinkModFramework", "com.cneicy.shrink-mod-framework", "0.2.0"), + new ShrinkSdkPackageEntry("ShrinkNetwork", "com.cneicy.shrink-network", "0.2.0"), + new ShrinkSdkPackageEntry("ShrinkNetwork Integration App", "com.cneicy.shrink-network-integration-app", "0.1.0"), + new ShrinkSdkPackageEntry("ShrinkNetwork Integration EventBus", "com.cneicy.shrink-network-integration-eventbus", "0.1.1"), + new ShrinkSdkPackageEntry("ShrinkShared CodeGen", "com.cneicy.shrink-shared-codegen", "0.1.0"), + new ShrinkSdkPackageEntry("ShrinkTutorial", "com.cneicy.shrink-tutorial", "0.1.0") + }; + } +} +#endif diff --git a/README.md b/README.md new file mode 100644 index 0000000..8cb2305 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# ShrinkSDK Installer + +`com.cneicy.shrink-installer` is the Git URL bootstrap package for ShrinkSDK. + +Install the package from an immutable tag, then open `ShrinkSDK/Packages` in the Unity editor. The window merges the public ShrinkSDK scoped registry without replacing other registries and installs only catalogued package versions. + +The installer does not copy templates into `Assets`, does not store credentials, and does not add an unpinned package version. + +## Bootstrap URL + +```text +https://git.crash.work/ShrinkSDK/Installer.git#v0.1.0 +``` diff --git a/package.json b/package.json new file mode 100644 index 0000000..b681009 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "com.cneicy.shrink-installer", + "version": "0.1.0", + "displayName": "ShrinkSDK Installer", + "description": "ShrinkSDK registry bootstrapper and fixed-version package installer.", + "unity": "2022.3", + "documentationUrl": "https://git.crash.work/ShrinkSDK/Installer", + "dependencies": { + "com.unity.nuget.newtonsoft-json": "3.2.2" + }, + "keywords": [ + "shrink", + "installer", + "upm" + ], + "author": { + "name": "cneicy", + "url": "https://git.crash.work/ShrinkSDK" + } +}