diff --git a/.gitea/workflows/publish-nuget.yml b/.gitea/workflows/publish-nuget.yml new file mode 100644 index 0000000..06dc7b7 --- /dev/null +++ b/.gitea/workflows/publish-nuget.yml @@ -0,0 +1,52 @@ +name: Publish NuGet packages + +on: + push: + tags: + - 'v*' + workflow_dispatch: + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + env: + NUGET_AUTH_TOKEN: ${{ secrets.SHRINKSDK_PACKAGE_TOKEN }} + steps: + - name: Checkout tagged source + uses: actions/checkout@v4 + + - name: Set up .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Validate tag and pack projects + env: + GITEA_REF: ${{ gitea.ref }} + shell: bash + run: | + set -euo pipefail + tag="${GITEA_REF#refs/tags/}" + version="$(node -p "require('./package.json').version")" + test "$tag" = "v$version" + mkdir -p packages + mapfile -d '' projects < <(find DotNet~ Godot~ -type f -name '*.csproj' -print0 2>/dev/null || true) + test "${#projects[@]}" -gt 0 + for project in "${projects[@]}"; do + dotnet restore "$project" --configfile NuGet.Config + dotnet pack "$project" --configuration Release --no-restore --output "$PWD/packages" --include-symbols --include-source + done + find packages -maxdepth 1 -name '*.nupkg' -type f | grep -q . + + - name: Publish packages + shell: bash + run: | + set -euo pipefail + : "${NUGET_AUTH_TOKEN:?SHRINKSDK_PACKAGE_TOKEN is required}" + dotnet nuget push 'packages/*.nupkg' --api-key "$NUGET_AUTH_TOKEN" --source https://git.crash.work/api/packages/ShrinkSDK/nuget/index.json --skip-duplicate + if compgen -G 'packages/*.snupkg' > /dev/null; then + dotnet nuget push 'packages/*.snupkg' --api-key "$NUGET_AUTH_TOKEN" --source https://git.crash.work/api/packages/ShrinkSDK/nuget/index.json --skip-duplicate + fi diff --git a/.gitignore b/.gitignore index 246935d..c3b8764 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,11 @@ /Tools~/**/[Oo]bj/ *.user *.DotSettings.user +/DotNet~/**/[Bb]in/ +/DotNet~/**/[Oo]bj/ +/Godot~/**/[Bb]in/ +/Godot~/**/[Oo]bj/ +/artifacts/ +/packages/ +!DotNet~/**/*.csproj +!Godot~/**/*.csproj diff --git a/.npmignore b/.npmignore index 700f03d..6a59c77 100644 --- a/.npmignore +++ b/.npmignore @@ -6,3 +6,7 @@ Tools~/ *.sln *.user *.DotSettings.user +DotNet~/ +Godot~/ +NuGet.Config +Directory.Build.props diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..43b188b --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,13 @@ + + + latest + enable + true + true + ShrinkSDK + ShrinkSDK + https://git.crash.work/ShrinkSDK + true + snupkg + + diff --git a/Editor/ShrinkSdkPackageCatalog.cs b/Editor/ShrinkSdkPackageCatalog.cs index 02dc959..17cfd30 100644 --- a/Editor/ShrinkSdkPackageCatalog.cs +++ b/Editor/ShrinkSdkPackageCatalog.cs @@ -73,7 +73,7 @@ namespace ShrinkSDK.Installer "com.cneicy.shrink-context-app-adapter", "0.2.0", ShrinkSdkPackageLayer.Context, true); internal static readonly ShrinkSdkPackageEntry ContextEventBusAdapter = Package( "Context 事件适配", "把事件订阅纳入 Context 的安装与自动撤回。", - "com.cneicy.shrink-context-eventbus-adapter", "0.1.1", ShrinkSdkPackageLayer.Context, true); + "com.cneicy.shrink-context-eventbus-adapter", "0.2.0", ShrinkSdkPackageLayer.Context, true); internal static readonly ShrinkSdkPackageEntry AppCore = Package( "应用宿主", "统一启动、服务门面与应用生命周期。", @@ -127,7 +127,7 @@ namespace ShrinkSDK.Installer "com.cneicy.shrink-network-integration-eventbus", "0.2.0", ShrinkSdkPackageLayer.Integration, true); internal static readonly ShrinkSdkPackageEntry SharedCodeGen = Package( "共享代码生成", "各功能包使用的编译期注册表基础设施。", - "com.cneicy.shrink-shared-codegen", "0.1.0", ShrinkSdkPackageLayer.Tooling); + "com.cneicy.shrink-shared-codegen", "0.1.1", ShrinkSdkPackageLayer.Tooling); internal static readonly ShrinkSdkPackageEntry RuntimeAbstractions = Package( "运行时抽象", "Unity 与其他 .NET 宿主共用的平台服务合同。", "com.cneicy.shrink-runtime-abstractions", "0.1.0", ShrinkSdkPackageLayer.Tooling); diff --git a/Godot~/ShrinkSDK.Godot.Installer.csproj b/Godot~/ShrinkSDK.Godot.Installer.csproj new file mode 100644 index 0000000..69ba4fe --- /dev/null +++ b/Godot~/ShrinkSDK.Godot.Installer.csproj @@ -0,0 +1,19 @@ + + + net8.0 + false + ShrinkSDK.Godot.Installer + ShrinkSDK.Godot.Installer + 0.1.1 + Godot Editor package manager and CodeGen diagnostics for ShrinkSDK. + + + + + + + + + + + diff --git a/Godot~/addons/shrinksdk/ShrinkGodotInstallerPlugin.cs b/Godot~/addons/shrinksdk/ShrinkGodotInstallerPlugin.cs new file mode 100644 index 0000000..ec1cc0a --- /dev/null +++ b/Godot~/addons/shrinksdk/ShrinkGodotInstallerPlugin.cs @@ -0,0 +1,133 @@ +#nullable enable + +using System; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using Godot; +using ShrinkSDK.Installer; + +namespace ShrinkSDK.Godot.Editor; + +[Tool] +public partial class ShrinkGodotInstallerPlugin : EditorPlugin +{ + private EditorDock? _panel; + private Label? _status; + private RichTextLabel? _output; + private readonly System.Collections.Generic.Dictionary _installActions = + new(StringComparer.OrdinalIgnoreCase); + private readonly System.Collections.Generic.Dictionary _removeActions = + new(StringComparer.OrdinalIgnoreCase); + private string ProjectDirectory => ProjectSettings.GlobalizePath("res://"); + private string ProjectPath => Directory.GetFiles(ProjectDirectory, "*.csproj", SearchOption.TopDirectoryOnly).Single(); + + public override void _EnterTree() + { + _panel = new EditorDock { Name = "ShrinkSDK", Title = "ShrinkSDK", DefaultSlot = EditorDock.DockSlot.LeftBr }; + var content = new VBoxContainer(); + _panel.AddChild(content); + content.AddChild(new Label { Text = "ShrinkSDK Packages" }); + _installActions.Clear(); + _removeActions.Clear(); + foreach (var package in ShrinkProjectPackageEditor.RecommendedPackages) + { + var row = new HBoxContainer(); + row.AddChild(new Label { Text = $"{package.Key} {package.Value}", SizeFlagsHorizontal = Control.SizeFlags.ExpandFill }); + var install = new Button { Text = "Install / Update" }; + install.Pressed += () => ChangePackage(package.Key, package.Value); + row.AddChild(install); + _installActions[package.Key] = install; + var remove = new Button { Text = "Remove" }; + remove.Pressed += () => ChangePackage(package.Key, null); + row.AddChild(remove); + _removeActions[package.Key] = remove; + content.AddChild(row); + } + var restore = new Button { Text = "Restore and Build" }; + restore.Pressed += BuildProject; + content.AddChild(restore); + _status = new Label { Text = "CodeGen: not built" }; + content.AddChild(_status); + _output = new RichTextLabel { FitContent = true, CustomMinimumSize = new Vector2(360, 180) }; + content.AddChild(_output); + AddDock(_panel); + RefreshInstalledState(); + } + + public override void _ExitTree() + { + if (_panel == null) return; + RemoveDock(_panel); + _panel.QueueFree(); + _panel = null; + } + + private async void ChangePackage(string packageId, string? version) + { + try + { + ShrinkProjectPackageEditor.EnsureShrinkFeed(Path.Combine(ProjectDirectory, "NuGet.Config")); + ShrinkProjectPackageEditor.SetPackageReference(ProjectPath, packageId, version); + await RunBuild("restore"); + } + catch (Exception exception) { ShowFailure(exception); } + } + + private async void BuildProject() + { + try { await RunBuild("build"); } + catch (Exception exception) { ShowFailure(exception); } + } + + private async System.Threading.Tasks.Task RunBuild(string command) + { + _status!.Text = $"CodeGen: running dotnet {command}"; + var result = await ShrinkProjectPackageEditor.RunDotnetAsync(ProjectDirectory, $"{command} \"{ProjectPath}\" /nr:false"); + _output!.Text = result.Output; + var match = Regex.Matches(result.Output, @"\[ShrinkSDK\.CodeGen\].*woven: instance=(\d+), static=(\d+), registry=(\d+)").Cast().LastOrDefault(); + _status.Text = result.ExitCode == 0 + ? match == null ? "CodeGen: build succeeded (no attributed registrations)" : $"CodeGen: woven; instance={match.Groups[1].Value}, static={match.Groups[2].Value}, registry={match.Groups[3].Value}" + : "CodeGen: build failed"; + RefreshInstalledState(); + } + + private void RefreshInstalledState() + { + if (_output == null) return; + var installed = File.Exists(ProjectPath) + ? ShrinkProjectPackageEditor.ReadPackageReferences(ProjectPath) + : new System.Collections.Generic.Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var package in ShrinkProjectPackageEditor.RecommendedPackages) + { + var hasInstalled = installed.TryGetValue(package.Key, out var installedVersion); + var relation = hasInstalled + ? ShrinkPackageVersion.Classify(installedVersion, package.Value) + : ShrinkPackageVersionRelation.Unknown; + if (_installActions.TryGetValue(package.Key, out var install)) + { + install.Text = !hasInstalled + ? $"Install {package.Value}" + : relation switch + { + ShrinkPackageVersionRelation.Equal => $"Installed {installedVersion}", + ShrinkPackageVersionRelation.Newer => $"Newer {installedVersion}", + ShrinkPackageVersionRelation.Older => $"Upgrade {installedVersion} → {package.Value}", + _ => $"Update {installedVersion} → {package.Value}" + }; + install.Disabled = hasInstalled && relation is + ShrinkPackageVersionRelation.Equal or ShrinkPackageVersionRelation.Newer; + } + if (_removeActions.TryGetValue(package.Key, out var remove)) + remove.Disabled = !hasInstalled; + } + _output.Text = "Direct packages: " + string.Join(", ", installed.Select(item => $"{item.Key}@{item.Value}")); + } + + private void ShowFailure(Exception exception) + { + _status!.Text = "CodeGen: operation failed"; + _output!.Text = exception.ToString(); + GD.PushError(exception.ToString()); + } +} diff --git a/Godot~/addons/shrinksdk/ShrinkProjectPackageEditor.cs b/Godot~/addons/shrinksdk/ShrinkProjectPackageEditor.cs new file mode 100644 index 0000000..7397f76 --- /dev/null +++ b/Godot~/addons/shrinksdk/ShrinkProjectPackageEditor.cs @@ -0,0 +1,148 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Xml.Linq; + +namespace ShrinkSDK.Godot.Editor; + +public static class ShrinkProjectPackageEditor +{ + public static readonly IReadOnlyDictionary RecommendedPackages = + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["ShrinkSDK.Godot"] = "0.1.0", + ["ShrinkSDK.EventBus"] = "2.1.0", + ["ShrinkSDK.Context.Core"] = "0.2.0", + ["ShrinkSDK.Command"] = "0.3.0", + ["ShrinkSDK.Command.Integration.EventBus"] = "0.2.0", + ["ShrinkSDK.Command.Integration.Network"] = "0.2.0", + ["ShrinkSDK.Command.Integration.App"] = "0.2.0", + ["ShrinkSDK.Network"] = "0.3.0", + ["ShrinkSDK.Network.Integration.EventBus"] = "0.2.0", + ["ShrinkSDK.Network.Integration.App"] = "0.2.0", + ["ShrinkSDK.DataSaver"] = "2.3.0", + ["ShrinkSDK.DataSaver.Integration.EventBus"] = "2.2.0", + ["ShrinkSDK.DataSaver.Integration.App"] = "0.2.0", + ["ShrinkSDK.App.Core"] = "0.2.0", + ["ShrinkSDK.Context.AppAdapter"] = "0.2.0", + ["ShrinkSDK.App.Starter.Basic"] = "0.3.0", + ["ShrinkSDK.ModFramework"] = "0.3.0", + ["ShrinkSDK.ModFramework.Godot"] = "0.1.0", + ["ShrinkSDK.Tutorial"] = "0.2.0", + ["ShrinkSDK.Tutorial.Godot"] = "0.1.0" + }; + + public static IReadOnlyDictionary ReadPackageReferences(string projectPath) + { + var document = XDocument.Load(projectPath, LoadOptions.PreserveWhitespace); + return document.Descendants("PackageReference") + .Select(element => new + { + Id = ((string?)element.Attribute("Include") ?? (string?)element.Attribute("Update"))?.Trim(), + Version = ((string?)element.Attribute("Version") ?? + (string?)element.Attribute("VersionOverride") ?? + element.Element("Version")?.Value)?.Trim() ?? string.Empty + }) + .Where(item => !string.IsNullOrWhiteSpace(item.Id)) + .GroupBy(item => item.Id!, StringComparer.OrdinalIgnoreCase) + .ToDictionary(group => group.Key, group => group.First().Version, StringComparer.OrdinalIgnoreCase); + } + + public static void SetPackageReference(string projectPath, string packageId, string? version) + { + var document = XDocument.Load(projectPath, LoadOptions.PreserveWhitespace); + var root = document.Root ?? throw new InvalidDataException("The C# project has no root element."); + var references = root.Descendants("PackageReference").Where(element => + string.Equals((string?)element.Attribute("Include"), packageId, StringComparison.OrdinalIgnoreCase)).ToArray(); + foreach (var duplicate in references.Skip(1)) duplicate.Remove(); + if (string.IsNullOrWhiteSpace(version)) + { + foreach (var reference in references) reference.Remove(); + } + else if (references.Length > 0) + { + var reference = references[0]; + if (reference.Attribute("VersionOverride") != null) + reference.SetAttributeValue("VersionOverride", version); + else if (reference.Attribute("Version") != null) + reference.SetAttributeValue("Version", version); + else if (reference.Element("Version") != null) + reference.Element("Version")!.Value = version; + else + reference.SetAttributeValue("Version", version); + } + else + { + var group = root.Elements("ItemGroup").FirstOrDefault(element => element.Elements("PackageReference").Any()); + if (group == null) + { + group = new XElement("ItemGroup"); + root.Add(group); + } + group.Add(new XElement("PackageReference", new XAttribute("Include", packageId), new XAttribute("Version", version))); + } + WriteAtomically(projectPath, document); + } + + public static void EnsureShrinkFeed(string nugetConfigPath) + { + XDocument document; + if (File.Exists(nugetConfigPath)) document = XDocument.Load(nugetConfigPath, LoadOptions.PreserveWhitespace); + else document = new XDocument(new XDeclaration("1.0", "utf-8", null), new XElement("configuration")); + var root = document.Root ?? throw new InvalidDataException("NuGet.Config has no root element."); + var sources = root.Element("packageSources"); + if (sources == null) + { + sources = new XElement("packageSources"); + root.Add(sources); + } + var existing = sources.Elements("add").FirstOrDefault(element => + string.Equals((string?)element.Attribute("key"), "ShrinkSDK", StringComparison.OrdinalIgnoreCase)); + if (existing == null) + sources.Add(new XElement("add", new XAttribute("key", "ShrinkSDK"), + new XAttribute("value", "https://git.crash.work/api/packages/ShrinkSDK/nuget/index.json"))); + else + existing.SetAttributeValue("value", "https://git.crash.work/api/packages/ShrinkSDK/nuget/index.json"); + WriteAtomically(nugetConfigPath, document); + } + + public static async Task<(int ExitCode, string Output)> RunDotnetAsync(string projectDirectory, + string arguments, CancellationToken cancellationToken = default) + { + var output = new StringBuilder(); + using var process = new Process + { + StartInfo = new ProcessStartInfo("dotnet", arguments) + { + WorkingDirectory = projectDirectory, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + } + }; + process.OutputDataReceived += (_, args) => { if (args.Data != null) output.AppendLine(args.Data); }; + process.ErrorDataReceived += (_, args) => { if (args.Data != null) output.AppendLine(args.Data); }; + process.Start(); + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + await process.WaitForExitAsync(cancellationToken); + return (process.ExitCode, output.ToString()); + } + + private static void WriteAtomically(string path, XDocument document) + { + Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(path))!); + var temporaryPath = path + ".shrinksdk.tmp"; + using (var writer = new StreamWriter(temporaryPath, false, new UTF8Encoding(false))) + document.Save(writer, SaveOptions.DisableFormatting); + File.Move(temporaryPath, path, true); + } +} diff --git a/Godot~/addons/shrinksdk/plugin.cfg b/Godot~/addons/shrinksdk/plugin.cfg new file mode 100644 index 0000000..1715616 --- /dev/null +++ b/Godot~/addons/shrinksdk/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="ShrinkSDK Installer" +description="Installs and diagnoses ShrinkSDK NuGet modules and CodeGen weaving." +author="ShrinkSDK" +version="0.1.1" +script="ShrinkGodotInstallerPlugin.cs" diff --git a/NuGet.Config b/NuGet.Config new file mode 100644 index 0000000..43e134b --- /dev/null +++ b/NuGet.Config @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/README.md b/README.md index 2581da7..bf61f5f 100644 --- a/README.md +++ b/README.md @@ -19,3 +19,13 @@ https://git.crash.work/ShrinkSDK/Installer.git#main ``` 正式项目应在对应版本发布后将 `main` 换成固定 tag。安装完成后等待 Unity 编译,再打开 `ShrinkSDK/包管理`,选择单个模块或推荐组合。 + +## Godot 4.6 C# + +Godot Installer 源码和 NuGet 打包工程位于 `Godot~`。安装 `ShrinkSDK.Godot.Installer 0.1.1` 后,将包内 `contentFiles/any/any/addons/shrinksdk` 复制到 Godot 项目的 `addons/shrinksdk`,再到 `Project > Project Settings > Plugins` 启用 **ShrinkSDK Installer**。 + +Godot 面板直接修改项目 `.csproj` 的 `PackageReference`,安全合并项目级 `NuGet.Config`,并可执行 `dotnet restore/build`、显示 CodeGen 织入状态。已安装版本高于目录版本时不会执行降级。 + +```powershell +dotnet pack .\Godot~\ShrinkSDK.Godot.Installer.csproj -c Release --output .\artifacts --include-symbols --include-source +```