commit 235bf38f5864d3329b7bfc51fe016ccc4791a436 Author: cneicy Date: Wed Aug 26 02:50:40 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..6a101ba --- /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/ShrinkShared.CodeGen.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..3270236 --- /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/ShrinkShared.CodeGen.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..783fc72 --- /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-shared-codegen": "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.meta b/Editor.meta new file mode 100644 index 0000000..64affae --- /dev/null +++ b/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7881ad37b016fc84ea31b463403d901a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/PostProcessorAssemblyResolver.cs b/Editor/PostProcessorAssemblyResolver.cs new file mode 100644 index 0000000..0a6c68c --- /dev/null +++ b/Editor/PostProcessorAssemblyResolver.cs @@ -0,0 +1,84 @@ +#nullable enable + +using System.Collections.Generic; +using System.IO; +using System.Threading; +using Mono.Cecil; +using Unity.CompilationPipeline.Common.ILPostProcessing; + +namespace ShrinkShared.CodeGen +{ + internal sealed class PostProcessorAssemblyResolver : IAssemblyResolver + { + private readonly string[] _references; + private readonly Dictionary _cache = new(); + private AssemblyDefinition? _self; + + public PostProcessorAssemblyResolver(ICompiledAssembly compiledAssembly) + { + _references = compiledAssembly.References; + } + + public void AddAssemblyDefinitionBeingOperatedOn(AssemblyDefinition assemblyDefinition) + { + _self = assemblyDefinition; + } + + public AssemblyDefinition? Resolve(AssemblyNameReference name) + => Resolve(name, new ReaderParameters(ReadingMode.Deferred)); + + public AssemblyDefinition? Resolve(AssemblyNameReference name, ReaderParameters parameters) + { + lock (_cache) + { + if (name.Name == _self?.Name.Name) + return _self; + + var path = FindPath(name); + if (path == null) + return null; + + var key = $"{path}{File.GetLastWriteTime(path)}"; + if (_cache.TryGetValue(key, out var cached)) + return cached; + + parameters.AssemblyResolver = this; + var assembly = AssemblyDefinition.ReadAssembly(ReadFileWithRetry(path), parameters); + _cache[key] = assembly; + return assembly; + } + } + + private string? FindPath(AssemblyNameReference name) + { + foreach (var reference in _references) + { + if (Path.GetFileNameWithoutExtension(reference) == name.Name) + return reference; + } + + return null; + } + + private static MemoryStream ReadFileWithRetry(string path, int retries = 5) + { + for (var i = 0; i < retries; i++) + { + try + { + return new MemoryStream(File.ReadAllBytes(path)); + } + catch (IOException) when (i < retries - 1) + { + Thread.Sleep(100); + } + } + + throw new IOException($"[ShrinkShared.CodeGen] 无法读取文件: {path}"); + } + + public void Dispose() + { + } + } +} diff --git a/Editor/PostProcessorAssemblyResolver.cs.meta b/Editor/PostProcessorAssemblyResolver.cs.meta new file mode 100644 index 0000000..50b26e1 --- /dev/null +++ b/Editor/PostProcessorAssemblyResolver.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b750fb992bcdddc479a883b1b6bf5ef2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/PostProcessorReflectionImporter.cs b/Editor/PostProcessorReflectionImporter.cs new file mode 100644 index 0000000..38ab4aa --- /dev/null +++ b/Editor/PostProcessorReflectionImporter.cs @@ -0,0 +1,34 @@ +#nullable enable + +using System.Linq; +using System.Reflection; +using Mono.Cecil; + +namespace ShrinkShared.CodeGen +{ + internal sealed class PostProcessorReflectionImporter : DefaultReflectionImporter + { + private const string CoreLibName = "System.Private.CoreLib"; + private readonly AssemblyNameReference? _corlib; + + public PostProcessorReflectionImporter(ModuleDefinition module) + : base(module) + { + _corlib = module.AssemblyReferences.FirstOrDefault(reference => reference.Name is "mscorlib" or "netstandard"); + } + + public override AssemblyNameReference ImportReference(AssemblyName reference) + { + if (_corlib != null && reference.Name == CoreLibName) + return _corlib; + + return base.ImportReference(reference); + } + } + + internal sealed class PostProcessorReflectionImporterProvider : IReflectionImporterProvider + { + public IReflectionImporter GetReflectionImporter(ModuleDefinition module) + => new PostProcessorReflectionImporter(module); + } +} diff --git a/Editor/PostProcessorReflectionImporter.cs.meta b/Editor/PostProcessorReflectionImporter.cs.meta new file mode 100644 index 0000000..70a11d8 --- /dev/null +++ b/Editor/PostProcessorReflectionImporter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 31170a0a3448b8f428b07fa251fe5d10 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/ShrinkRegistryILPostProcessor.cs b/Editor/ShrinkRegistryILPostProcessor.cs new file mode 100644 index 0000000..dd868b4 --- /dev/null +++ b/Editor/ShrinkRegistryILPostProcessor.cs @@ -0,0 +1,192 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Mono.Cecil; +using Mono.Cecil.Pdb; +using Unity.CompilationPipeline.Common.Diagnostics; +using Unity.CompilationPipeline.Common.ILPostProcessing; + +namespace ShrinkShared.CodeGen +{ + public sealed class ShrinkRegistryILPostProcessor : ILPostProcessor + { + public override ILPostProcessor GetInstance() => this; + + public override bool WillProcess(ICompiledAssembly compiledAssembly) + { + // 注意:不能因为"引用了 ShrinkEventBus.Runtime"就处理该程序集。 + // 一旦把 ShrinkApp.Core.Runtime 等核心程序集卷入 Cecil 读写,写回的 dll + // 会被 Unity 判定为 "references itself" 而整条依赖链拒绝加载。 + // 只处理直接承载 Command/Network/App 注册表的程序集。 + return compiledAssembly.References.Any(path => + Path.GetFileNameWithoutExtension(path) is "ShrinkCommand.Runtime" or "ShrinkNetwork.Runtime" + or "ShrinkApp.Core.Runtime"); + } + + public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly) + { + var diagnostics = new List(); + if (!WillProcess(compiledAssembly)) + return new ILPostProcessResult(compiledAssembly.InMemoryAssembly, diagnostics); + + var assemblyDefinition = AssemblyDefinitionFor(compiledAssembly); + var module = assemblyDefinition.MainModule; + + try + { + InjectShrinkCommandRegistry(module); + InjectShrinkNetworkRegistry(module); + InjectShrinkAppRegistry(module); + } + catch (Exception ex) + { + diagnostics.Add(new DiagnosticMessage + { + DiagnosticType = DiagnosticType.Error, + MessageData = $"[ShrinkShared.CodeGen] {ex.Message}" + }); + } + + return GetResult(assemblyDefinition, diagnostics); + } + + private void InjectShrinkCommandRegistry(ModuleDefinition module) + { + var subscriberType = FindType(module, "ShrinkCommand.ShrinkCommandSubscriberAttribute", "ShrinkCommand.Runtime"); + var commandAttributeType = FindType(module, "ShrinkCommand.ShrinkCommandAttribute", "ShrinkCommand.Runtime"); + var registryCtor = FindTypeArrayConstructor(module, + "ShrinkCommand.ShrinkCommandStaticRegistryAttribute", + "ShrinkCommand.Runtime"); + + if (subscriberType == null || commandAttributeType == null || registryCtor == null) + return; + + var subscriberTypes = module.Types + .Where(type => HasAttribute(type, subscriberType)) + .Where(type => type.Methods.Any(method => method.IsStatic && HasAttribute(method, commandAttributeType))) + .Select(type => module.ImportReference(type)) + .ToArray(); + + if (subscriberTypes.Length > 0) + AddAssemblyTypeArrayAttribute(module, registryCtor, subscriberTypes); + } + + private void InjectShrinkNetworkRegistry(ModuleDefinition module) + { + var messageAttributeType = FindType(module, "ShrinkNetwork.ShrinkNetworkMessageAttribute", "ShrinkNetwork.Runtime"); + var subscriberAttributeType = FindType(module, "ShrinkNetwork.ShrinkNetworkSubscriberAttribute", "ShrinkNetwork.Runtime"); + var subscribeAttributeType = FindType(module, "ShrinkNetwork.ShrinkNetworkSubscribeAttribute", "ShrinkNetwork.Runtime"); + var messageCtor = FindTypeArrayConstructor(module, "ShrinkNetwork.ShrinkNetworkMessageRegistryAttribute", "ShrinkNetwork.Runtime"); + var subscriberCtor = FindTypeArrayConstructor(module, "ShrinkNetwork.ShrinkNetworkStaticSubscriberRegistryAttribute", "ShrinkNetwork.Runtime"); + + if (messageAttributeType != null && messageCtor != null) + { + var messageTypes = module.Types + .Where(type => HasAttribute(type, messageAttributeType)) + .Select(type => module.ImportReference(type)) + .ToArray(); + if (messageTypes.Length > 0) + AddAssemblyTypeArrayAttribute(module, messageCtor, messageTypes); + } + + if (subscriberAttributeType != null && subscribeAttributeType != null && subscriberCtor != null) + { + var subscriberTypes = module.Types + .Where(type => HasAttribute(type, subscriberAttributeType)) + .Where(type => type.Methods.Any(method => method.IsStatic && HasAttribute(method, subscribeAttributeType))) + .Select(type => module.ImportReference(type)) + .ToArray(); + if (subscriberTypes.Length > 0) + AddAssemblyTypeArrayAttribute(module, subscriberCtor, subscriberTypes); + } + } + + private void InjectShrinkAppRegistry(ModuleDefinition module) + { + var installerAttributeType = FindType(module, "ShrinkApp.ShrinkAppModuleInstallerAttribute", "ShrinkApp.Core.Runtime"); + var installerInterfaceType = FindType(module, "ShrinkApp.IShrinkAppModuleInstaller", "ShrinkApp.Core.Runtime"); + var registryCtor = FindTypeArrayConstructor(module, "ShrinkApp.ShrinkAppInstallerRegistryAttribute", "ShrinkApp.Core.Runtime"); + + if (installerAttributeType == null || installerInterfaceType == null || registryCtor == null) + return; + + var installerTypes = module.Types + .Where(type => !type.IsAbstract) + .Where(type => HasAttribute(type, installerAttributeType)) + .Where(type => type.Interfaces.Any(item => item.InterfaceType.FullName == installerInterfaceType.FullName)) + .Select(type => module.ImportReference(type)) + .ToArray(); + + if (installerTypes.Length > 0) + AddAssemblyTypeArrayAttribute(module, registryCtor, installerTypes); + } + + private static bool HasAttribute(ICustomAttributeProvider provider, TypeReference expectedAttributeType) + { + return provider.CustomAttributes.Any(attribute => attribute.AttributeType.FullName == expectedAttributeType.FullName); + } + + private static void AddAssemblyTypeArrayAttribute(ModuleDefinition module, MethodReference ctor, TypeReference[] types) + { + var attribute = new CustomAttribute(ctor); + var typeTypeRef = module.ImportReference(typeof(Type)); + attribute.ConstructorArguments.Add(new CustomAttributeArgument( + module.ImportReference(typeof(Type[])), + types.Select(type => new CustomAttributeArgument(typeTypeRef, type)).ToArray())); + module.Assembly.CustomAttributes.Add(attribute); + } + + private static TypeReference? FindType(ModuleDefinition module, string fullName, string assemblyName) + { + var resolved = Type.GetType($"{fullName}, {assemblyName}", false); + return resolved == null ? null : module.ImportReference(resolved); + } + + private static MethodReference? FindTypeArrayConstructor(ModuleDefinition module, string fullName, string assemblyName) + { + var typeRef = FindType(module, fullName, assemblyName); + var typeDef = typeRef?.Resolve(); + var ctor = typeDef?.Methods.FirstOrDefault(method => + method.IsConstructor && + method.Parameters.Count == 1 && + method.Parameters[0].ParameterType.IsArray && + method.Parameters[0].ParameterType.GetElementType().FullName == module.ImportReference(typeof(Type)).FullName); + return ctor == null ? null : module.ImportReference(ctor); + } + + private static AssemblyDefinition AssemblyDefinitionFor(ICompiledAssembly compiledAssembly) + { + var assemblyResolver = new PostProcessorAssemblyResolver(compiledAssembly); + var readerParameters = new ReaderParameters + { + SymbolStream = new MemoryStream(compiledAssembly.InMemoryAssembly.PdbData.ToArray()), + SymbolReaderProvider = new PdbReaderProvider(), + AssemblyResolver = assemblyResolver, + ReflectionImporterProvider = new PostProcessorReflectionImporterProvider(), + ReadingMode = ReadingMode.Immediate + }; + + var assemblyDefinition = AssemblyDefinition.ReadAssembly( + new MemoryStream(compiledAssembly.InMemoryAssembly.PeData.ToArray()), + readerParameters); + assemblyResolver.AddAssemblyDefinitionBeingOperatedOn(assemblyDefinition); + return assemblyDefinition; + } + + private static ILPostProcessResult GetResult(AssemblyDefinition assemblyDefinition, List diagnostics) + { + var pe = new MemoryStream(); + var pdb = new MemoryStream(); + assemblyDefinition.Write(pe, new WriterParameters + { + SymbolWriterProvider = new PdbWriterProvider(), + SymbolStream = pdb, + WriteSymbols = true + }); + return new ILPostProcessResult(new InMemoryAssembly(pe.ToArray(), pdb.ToArray()), diagnostics); + } + } +} diff --git a/Editor/ShrinkRegistryILPostProcessor.cs.meta b/Editor/ShrinkRegistryILPostProcessor.cs.meta new file mode 100644 index 0000000..acc03df --- /dev/null +++ b/Editor/ShrinkRegistryILPostProcessor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0f07f372d8ceeed47aa4e5702fd1adef +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/ShrinkShared.CodeGen.asmdef b/Editor/ShrinkShared.CodeGen.asmdef new file mode 100644 index 0000000..579aaff --- /dev/null +++ b/Editor/ShrinkShared.CodeGen.asmdef @@ -0,0 +1,21 @@ +{ + "name": "Unity.ShrinkShared.CodeGen", + "rootNamespace": "ShrinkShared.CodeGen", + "references": [], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": true, + "overrideReferences": true, + "precompiledReferences": [ + "Mono.Cecil.dll", + "Mono.Cecil.Mdb.dll", + "Mono.Cecil.Pdb.dll", + "Mono.Cecil.Rocks.dll" + ], + "autoReferenced": false, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/Editor/ShrinkShared.CodeGen.asmdef.meta b/Editor/ShrinkShared.CodeGen.asmdef.meta new file mode 100644 index 0000000..23e4c60 --- /dev/null +++ b/Editor/ShrinkShared.CodeGen.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 89b1954231cba2541ab97b45f6a407fe +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/README.md b/README.md new file mode 100644 index 0000000..a2ad718 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# Shrink Shared CodeGen + +Editor-only UPM 包,为引用 ShrinkCommand.Runtime、ShrinkNetwork.Runtime 或 ShrinkApp.Core.Runtime 的程序集生成程序集级注册表。 + +该包只通过 Cecil 按程序集名和类型全名读取元数据,不在 asmdef 或 UPM 层反向引用业务包。因此 App、Command、Network 可以依赖它而不形成循环依赖。运行时不包含此包代码。 diff --git a/README.md.meta b/README.md.meta new file mode 100644 index 0000000..9033778 --- /dev/null +++ b/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: c9ae00f51ad2a3e4a82845953bf8bff9 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/package.json b/package.json new file mode 100644 index 0000000..44fa3ef --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "com.cneicy.shrink-shared-codegen", + "version": "0.1.0", + "displayName": "Shrink Shared CodeGen", + "description": "ShrinkApp、ShrinkCommand 与 ShrinkNetwork 共用的 Unity IL 后处理注册表生成器。", + "unity": "2022.3", + "dependencies": { + "com.unity.nuget.mono-cecil": "1.11.4" + }, + "keywords": [ + "codegen", + "ilpp", + "registry" + ], + "author": { + "name": "cneicy", + "url": "https://git.crash.work/ShrinkSDK" + }, + "documentationUrl": "https://git.crash.work/ShrinkSDK/ShrinkShared.CodeGen" +} diff --git a/package.json.meta b/package.json.meta new file mode 100644 index 0000000..51a2018 --- /dev/null +++ b/package.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 12ee30d453322c346b13f068125681d5 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: