Compare commits

...
35 Commits
Author SHA1 Message Date
cneicy f92105c97d fix: use self-contained catalog runner image
Validate ShrinkSDK Workspace / catalog (push) Successful in 37s
Validate ShrinkSDK Workspace / unity (push) Successful in 3m7s
2026-09-05 07:02:12 +08:00
cneicy ddd55c0c58 fix: install git for catalog workflow jobs
Validate ShrinkSDK Workspace / catalog (push) Successful in 16s
Validate ShrinkSDK Workspace / unity (push) Successful in 3m10s
2026-09-05 06:46:44 +08:00
cneicy 2760dee6a7 fix: run catalog checks outside Unity containers
Validate ShrinkSDK Workspace / catalog (push) Failing after 1s
Validate ShrinkSDK Workspace / unity (push) Skipped
2026-09-05 06:44:57 +08:00
cneicy bb12b8d02d fix: automate package versions and workspace validation
Validate ShrinkSDK Workspace / unity (push) Failing after 5s
2026-09-05 06:32:13 +08:00
cneicy 6c90f9dc08 docs: finalize Godot package installation
Validate ShrinkSDK Workspace / unity (push) Failing after 4s
2026-09-05 04:33:49 +08:00
cneicy 7debc0240c chore: publish Godot and NuGet module releases
Validate ShrinkSDK Workspace / unity (push) Failing after 5s
2026-09-05 04:28:36 +08:00
cneicy e9d8e93427 refactor: move Godot packages into module repositories
Validate ShrinkSDK Workspace / unity (push) Failing after 5s
2026-09-05 03:43:14 +08:00
cneicy 2695dcdbba docs: add Unity and Godot setup guide
Validate ShrinkSDK Workspace / unity (push) Failing after 3m9s
2026-09-05 02:46:19 +08:00
cneicy 88ba3cdc48 feat: add Godot C# compatibility and shared codegen weaving
Validate ShrinkSDK Workspace / unity (push) Failing after 3m18s
2026-09-05 02:37:43 +08:00
cneicy 60cf457230 docs 2026-09-04 23:03:02 +08:00
cneicy b750ce7545 chore(workspace): adopt ShrinkTutorial 0.1.4
Validate ShrinkSDK Workspace / unity (push) Successful in 3m30s
2026-08-30 21:27:36 +08:00
cneicy 34e15e80e2 chore(workspace): adopt ShrinkTutorial 0.1.3
Validate ShrinkSDK Workspace / unity (push) Successful in 3m9s
2026-08-30 20:55:33 +08:00
cneicy de720ce33c chore(release): 发布 EventBus 2.0.1 依赖链
Validate ShrinkSDK Workspace / unity (push) Successful in 7m34s
2026-08-28 04:41:39 +08:00
cneicy 64248f5d4d chore(release): 对齐 ShrinkSDK 2026-08 稳定版本
Validate ShrinkSDK Workspace / unity (push) Failing after 2m59s
2026-08-28 03:49:52 +08:00
cneicy 0ba5c52e10 fix(ci): validate installer v0.2.2
Validate ShrinkSDK Workspace / unity (push) Successful in 3m7s
2026-08-26 13:29:44 +08:00
cneicy 9b8c528d23 fix(workspace): adopt installer v0.2.2
Validate ShrinkSDK Workspace / unity (push) Failing after 3m6s
2026-08-26 13:22:16 +08:00
cneicy 0bb94951b2 fix(workspace): adopt installer v0.2.1
Validate ShrinkSDK Workspace / unity (push) Successful in 3m21s
2026-08-26 13:01:53 +08:00
cneicy 2a513cbd5b feat(workspace): adopt context-first package management
Validate ShrinkSDK Workspace / unity (push) Successful in 3m7s
2026-08-26 12:49:24 +08:00
cneicy 3f46703d7a fix(workspace): update installer to v0.1.7
Validate ShrinkSDK Workspace / unity (push) Successful in 3m5s
2026-08-26 12:03:58 +08:00
cneicy d59b92b4dd fix(workspace): adopt installer diagnostics patch
Validate ShrinkSDK Workspace / unity (push) Successful in 3m5s
2026-08-26 12:00:41 +08:00
cneicy 9292d01f6a fix(ci): disambiguate package manager metadata
Validate ShrinkSDK Workspace / unity (push) Successful in 2m57s
2026-08-26 11:31:27 +08:00
cneicy f5f9cd76ec fix(ci): make installer consumer validation compatible
Validate ShrinkSDK Workspace / unity (push) Successful in 3m4s
2026-08-26 11:26:40 +08:00
cneicy 4a21bd75e3 fix(workspace): align published package versions
Validate ShrinkSDK Workspace / unity (push) Successful in 3m5s
2026-08-26 11:10:26 +08:00
cneicy bbce96426a fix(migration): validate minimal Unity consumers
Validate ShrinkSDK Workspace / unity (push) Failing after 3m10s
2026-08-26 11:06:24 +08:00
cneicy 7c0445606a fix(ci): use absolute Unity host paths
Validate ShrinkSDK Workspace / unity (push) Successful in 3m5s
2026-08-26 09:22:01 +08:00
cneicy a73773a0fb ci(workspace): diagnose package host verification
Validate ShrinkSDK Workspace / unity (push) Successful in 3m2s
2026-08-26 08:55:45 +08:00
cneicy efc9a2aca9 fix(migration): complete standalone package hosts
Validate ShrinkSDK Workspace / unity (push) Successful in 3m3s
2026-08-26 08:34:05 +08:00
cneicy 4d9b34dc36 chore(workspace): update EventBus tool project source
Validate ShrinkSDK Workspace / unity (push) Successful in 3m0s
2026-08-26 07:39:07 +08:00
cneicy f37aedf79f fix(workspace): align installer release version
Validate ShrinkSDK Workspace / unity (push) Successful in 3m0s
2026-08-26 07:24:00 +08:00
cneicy 178f7ae924 fix(ci): report Unity workspace failures
Validate ShrinkSDK Workspace / unity (push) Failing after 3m8s
2026-08-26 07:17:20 +08:00
cneicy add2af8ec1 ci(workspace): mirror Git package dependencies
Validate ShrinkSDK Workspace / unity (push) Failing after 3m1s
2026-08-26 07:08:30 +08:00
cneicy ce4517ed0c test(workspace): verify standalone package hosts
Validate ShrinkSDK Workspace / unity (push) Failing after 3m4s
2026-08-26 07:02:45 +08:00
cneicy 0dfdcca9ba fix(workspace): pin UniTask consumer source
Validate ShrinkSDK Workspace / unity (push) Failing after 2m28s
2026-08-26 06:48:31 +08:00
cneicy d1f0f44bd6 fix(installer): publish manifest-based bootstrap 2026-08-26 06:48:14 +08:00
cneicy 9c17781bbf ci(workspace): stabilize standalone package hosts 2026-08-26 06:47:35 +08:00
73 changed files with 3406 additions and 123 deletions
+105
View File
@@ -0,0 +1,105 @@
name: Verify ShrinkSDK Package Hosts
on:
workflow_dispatch:
jobs:
catalog:
runs-on: ubuntu-latest
container:
image: docker.m.daocloud.io/library/node:22-bookworm
permissions:
contents: read
steps:
- name: Validate submodules and version catalog
shell: bash
run: |
set -euo pipefail
ref="${{ gitea.sha }}"
git init .
git remote add origin "https://git.crash.work/ShrinkSDK/Workspace.git"
git fetch --depth=1 origin "$ref"
git checkout --detach FETCH_HEAD
git submodule sync --recursive
git submodule update --init --recursive
bash Tools/CI/Validate-Submodules.sh "$PWD"
node Tools/Release/update-shrinksdk-versions.mjs --check
editmode:
needs: catalog
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
permissions:
contents: read
env:
UNITY_VERSION: 2022.3.62f3
steps:
- name: Fetch exact Workspace revision and submodules
shell: bash
run: |
set -euo pipefail
ref="${{ gitea.sha }}"
git init .
git remote add origin "https://git.crash.work/ShrinkSDK/Workspace.git"
git fetch --depth=1 origin "$ref"
git checkout --detach FETCH_HEAD
git submodule sync --recursive
git submodule update --init --recursive
bash Tools/CI/Validate-Submodules.sh "$PWD"
- name: Run every standalone package EditMode host
shell: bash
run: |
set -euo pipefail
machine_id_file="/root/.local/share/unity3d/Unity/.machine-id"
if test -s "$machine_id_file"; then
cat "$machine_id_file" > /etc/machine-id
echo "Unity machine identity restored"
fi
git config --global url."https://ghfast.top/https://github.com/".insteadOf "https://github.com/"
unity_bin="$(command -v unity-editor || command -v unity || command -v Unity || true)"
test -n "$unity_bin"
"$unity_bin" -version | head -n 1 | grep -F "$UNITY_VERSION"
failed=0
workspace_root="$(pwd)"
for relative_host in Assets/Modules/*/Development~/UnityProject; do
host="$workspace_root/$relative_host"
package_root="$(dirname "$(dirname "$host")")"
package_name="$(basename "$package_root")"
results_dir="$host/TestResults"
results_file="$results_dir/editmode.xml"
log_file="$results_dir/unity.log"
mkdir -p "$results_dir"
echo "::group::${package_name} EditMode"
status=0
"$unity_bin" \
-batchmode \
-nographics \
-projectPath "$host" \
-runTests \
-testPlatform EditMode \
-testResults "$results_file" \
-logFile "$log_file" || status=$?
if test "$status" -ne 0 || ! test -s "$results_file" || ! grep -q 'result="Passed"' "$results_file"; then
failed=1
echo "${package_name} EditMode verification failed (Unity exit status: ${status})" >&2
if test -s "$results_file"; then
echo "${package_name} test result summary:" >&2
sed -n '1,40p' "$results_file" >&2
else
echo "${package_name} did not produce a test result file." >&2
fi
echo "${package_name} relevant Unity diagnostics:" >&2
grep -in -E 'error CS|compilation failed|test.*(failed|failure)|exception|fatal|no tests were executed|failed to' "$log_file" \
| tail -n 160 >&2 || true
else
echo "${package_name} EditMode verification passed"
fi
echo "::endgroup::"
done
exit "$failed"
@@ -0,0 +1,37 @@
name: Verify Published UPM Consumers
on:
workflow_dispatch:
jobs:
consumers:
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
permissions:
contents: read
steps:
- name: Fetch exact Workspace revision
shell: bash
run: |
set -euo pipefail
ref="${{ gitea.sha }}"
git init .
git remote add origin "https://git.crash.work/ShrinkSDK/Workspace.git"
git fetch --depth=1 origin "$ref"
git checkout --detach FETCH_HEAD
- name: Verify registry, Git tag, and installer consumers
shell: bash
run: |
set -euo pipefail
machine_id_file="/root/.local/share/unity3d/Unity/.machine-id"
if test -s "$machine_id_file"; then
cat "$machine_id_file" > /etc/machine-id
echo "Unity machine identity restored"
fi
git config --global url."https://ghfast.top/https://github.com/".insteadOf "https://github.com/"
bash Tools/UpmConsumerValidation/Validate-PublishedUpmConsumers.sh "$PWD"
+26 -4
View File
@@ -7,7 +7,29 @@ on:
workflow_dispatch: workflow_dispatch:
jobs: jobs:
catalog:
runs-on: ubuntu-latest
container:
image: docker.m.daocloud.io/library/node:22-bookworm
permissions:
contents: read
steps:
- name: Validate submodules and version catalog
shell: bash
run: |
set -euo pipefail
ref="${{ gitea.sha }}"
git init .
git remote add origin "https://git.crash.work/ShrinkSDK/Workspace.git"
git fetch --depth=1 origin "$ref"
git checkout --detach FETCH_HEAD
git submodule sync --recursive
git submodule update --init --recursive
bash Tools/CI/Validate-Submodules.sh "$PWD"
node Tools/Release/update-shrinksdk-versions.mjs --check
unity: unity:
needs: catalog
runs-on: unity-2022.3.62f3 runs-on: unity-2022.3.62f3
container: container:
image: docker.1panel.live/unityci/editor:ubuntu-2022.3.62f3-windows-mono-3 image: docker.1panel.live/unityci/editor:ubuntu-2022.3.62f3-windows-mono-3
@@ -30,10 +52,7 @@ jobs:
git checkout --detach FETCH_HEAD git checkout --detach FETCH_HEAD
git submodule sync --recursive git submodule sync --recursive
git submodule update --init --recursive git submodule update --init --recursive
module_count="$(git submodule status --recursive | wc -l | tr -d ' ')" bash Tools/CI/Validate-Submodules.sh "$PWD"
test "$module_count" = "21"
test -z "$(git submodule status --recursive | grep '^-')"
git submodule status --recursive
- name: Compile Workspace and validate package graph - name: Compile Workspace and validate package graph
shell: bash shell: bash
@@ -54,6 +73,7 @@ jobs:
cat "$machine_id_file" > /etc/machine-id cat "$machine_id_file" > /etc/machine-id
echo "Unity machine identity restored" echo "Unity machine identity restored"
fi fi
git config --global url."https://ghfast.top/https://github.com/".insteadOf "https://github.com/"
unity_bin="$(command -v unity-editor || command -v unity || command -v Unity || true)" unity_bin="$(command -v unity-editor || command -v unity || command -v Unity || true)"
test -n "$unity_bin" test -n "$unity_bin"
mkdir -p Artifacts mkdir -p Artifacts
@@ -66,5 +86,7 @@ jobs:
-projectPath "$PWD" \ -projectPath "$PWD" \
-executeMethod ShrinkSDK.WorkspaceValidation.ShrinkSdkWorkspaceValidation.Run \ -executeMethod ShrinkSDK.WorkspaceValidation.ShrinkSdkWorkspaceValidation.Run \
-logFile "$PWD/Artifacts/unity-workspace-validation.log" || status=$? -logFile "$PWD/Artifacts/unity-workspace-validation.log" || status=$?
echo '--- Unity workspace errors and exceptions ---'
grep -in -C 12 -E 'exception|buildfailed|build failed|error|failed|compilererror|invalidoperation|nullreference|package manager' "$PWD/Artifacts/unity-workspace-validation.log" | tail -n 260 || true
tail -n 240 "$PWD/Artifacts/unity-workspace-validation.log" || true tail -n 240 "$PWD/Artifacts/unity-workspace-validation.log" || true
exit "$status" exit "$status"
+3
View File
@@ -79,6 +79,8 @@ mono_crash.*
# namespace-suffixed Unity Integration.App packages in source control. # namespace-suffixed Unity Integration.App packages in source control.
!Assets/Modules/*.Integration.App/ !Assets/Modules/*.Integration.App/
!Assets/Modules/*.Integration.App/** !Assets/Modules/*.Integration.App/**
!Godot/Packages/*.Integration.App/
!Godot/Packages/*.Integration.App/**
# Crashlytics generated file # Crashlytics generated file
crashlytics-build.properties crashlytics-build.properties
@@ -143,3 +145,4 @@ GeneratedServers/ExampleGreedMod.Rules/*
!GeneratedServers/ExampleGreedMod.Rules/ExampleGreedMod.Rules.csproj !GeneratedServers/ExampleGreedMod.Rules/ExampleGreedMod.Rules.csproj
!GeneratedServers/ExampleGreedMod.Rules/pack-mod.ps1 !GeneratedServers/ExampleGreedMod.Rules/pack-mod.ps1
GeneratedModSdk/ GeneratedModSdk/
.mimosa/
+8
View File
@@ -82,3 +82,11 @@
path = Assets/Modules/ShrinkInstaller path = Assets/Modules/ShrinkInstaller
url = https://git.crash.work/ShrinkSDK/Installer.git url = https://git.crash.work/ShrinkSDK/Installer.git
branch = main branch = main
[submodule "Assets/Modules/ShrinkRuntime.Abstractions"]
path = Assets/Modules/ShrinkRuntime.Abstractions
url = https://git.crash.work/ShrinkSDK/ShrinkRuntime.Abstractions.git
branch = main
[submodule "Godot/Packages/ShrinkSDK.Godot"]
path = Godot/Packages/ShrinkSDK.Godot
url = https://git.crash.work/ShrinkSDK/ShrinkGodot.git
branch = main
+140 -51
View File
@@ -2,9 +2,12 @@
#nullable enable #nullable enable
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using UnityEditor; using UnityEditor;
using UnityEngine; using UnityEngine;
@@ -13,30 +16,11 @@ namespace ShrinkSDK.WorkspaceValidation
{ {
public static class ShrinkSdkWorkspaceValidation public static class ShrinkSdkWorkspaceValidation
{ {
private static readonly ExpectedPackage[] ExpectedPackages = private const string InstallerPackageName = "com.cneicy.shrink-installer";
{ private const string InstallerCatalogTypeName = "ShrinkSDK.Installer.ShrinkSdkPackageCatalog";
new ExpectedPackage("ShrinkApp.Core", "com.cneicy.shrink-app-core", "0.1.1"), private static readonly Regex SemanticVersion = new Regex(
new ExpectedPackage("ShrinkApp.Starter.Basic", "com.cneicy.shrink-app-starter-basic", "0.1.0"), @"^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$",
new ExpectedPackage("ShrinkCommand", "com.cneicy.shrink-command", "0.2.0"), RegexOptions.CultureInvariant);
new ExpectedPackage("ShrinkCommand.Integration.App", "com.cneicy.shrink-command-integration-app", "0.1.0"),
new ExpectedPackage("ShrinkCommand.Integration.EventBus", "com.cneicy.shrink-command-integration-eventbus", "0.1.1"),
new ExpectedPackage("ShrinkCommand.Integration.Network", "com.cneicy.shrink-command-integration-network", "0.1.0"),
new ExpectedPackage("ShrinkContext.AppAdapter", "com.cneicy.shrink-context-app-adapter", "0.1.0"),
new ExpectedPackage("ShrinkContext.Core", "com.cneicy.shrink-context-core", "0.1.0"),
new ExpectedPackage("ShrinkContext.EventBusAdapter", "com.cneicy.shrink-context-eventbus-adapter", "0.1.0"),
new ExpectedPackage("ShrinkDataSaver", "com.cneicy.shrink-datasaver", "2.2.0"),
new ExpectedPackage("ShrinkDataSaver.Integration.App", "com.cneicy.shrink-datasaver-integration-app", "0.1.0"),
new ExpectedPackage("ShrinkDataSaver.Integration.EventBus", "com.cneicy.shrink-datasaver-integration-eventbus", "2.1.0"),
new ExpectedPackage("ShrinkEventBus", "com.cneicy.shrink-eventbus", "2.0.0"),
new ExpectedPackage("ShrinkEventBus.Entities", "com.cneicy.shrink-eventbus-entities", "0.1.0"),
new ExpectedPackage("ShrinkInstaller", "com.cneicy.shrink-installer", "0.1.2"),
new ExpectedPackage("ShrinkModFramework", "com.cneicy.shrink-mod-framework", "0.2.1"),
new ExpectedPackage("ShrinkNetwork", "com.cneicy.shrink-network", "0.2.0"),
new ExpectedPackage("ShrinkNetwork.Integration.App", "com.cneicy.shrink-network-integration-app", "0.1.0"),
new ExpectedPackage("ShrinkNetwork.Integration.EventBus", "com.cneicy.shrink-network-integration-eventbus", "0.1.1"),
new ExpectedPackage("ShrinkShared.CodeGen", "com.cneicy.shrink-shared-codegen", "0.1.0"),
new ExpectedPackage("ShrinkTutorial", "com.cneicy.shrink-tutorial", "0.1.0")
};
public static void Run() public static void Run()
{ {
@@ -45,6 +29,7 @@ namespace ShrinkSDK.WorkspaceValidation
var workspaceRoot = Path.GetFullPath(Path.Combine(Application.dataPath, "..")); var workspaceRoot = Path.GetFullPath(Path.Combine(Application.dataPath, ".."));
var packages = ReadPackages(workspaceRoot); var packages = ReadPackages(workspaceRoot);
ValidateGraph(packages); ValidateGraph(packages);
ValidateInstallerCatalog(packages);
Debug.Log($"ShrinkSDK Workspace validation passed: packages={packages.Count}"); Debug.Log($"ShrinkSDK Workspace validation passed: packages={packages.Count}");
EditorApplication.Exit(0); EditorApplication.Exit(0);
} }
@@ -57,36 +42,97 @@ namespace ShrinkSDK.WorkspaceValidation
private static Dictionary<string, PackageDefinition> ReadPackages(string workspaceRoot) private static Dictionary<string, PackageDefinition> ReadPackages(string workspaceRoot)
{ {
var result = new Dictionary<string, PackageDefinition>(StringComparer.Ordinal); var modulesRoot = Path.Combine(workspaceRoot, "Assets", "Modules");
foreach (var expected in ExpectedPackages) if (!Directory.Exists(modulesRoot))
{ {
var manifestPath = Path.Combine(workspaceRoot, "Assets", "Modules", expected.Directory, "package.json"); throw new DirectoryNotFoundException($"Workspace package directory is missing: {modulesRoot}");
}
var declaredDirectories = ReadDeclaredPackageDirectories(workspaceRoot);
var manifestDirectories = Directory.EnumerateDirectories(modulesRoot)
.Where(directory => File.Exists(Path.Combine(directory, "package.json")))
.Select(Path.GetFullPath)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var undeclared = manifestDirectories
.Where(directory => !declaredDirectories.Contains(directory))
.OrderBy(directory => directory, StringComparer.OrdinalIgnoreCase)
.ToArray();
if (undeclared.Length > 0)
{
throw new InvalidOperationException(
"Package directories are not declared as submodules: " + string.Join(", ", undeclared));
}
var result = new Dictionary<string, PackageDefinition>(StringComparer.Ordinal);
foreach (var directory in declaredDirectories.OrderBy(value => value, StringComparer.OrdinalIgnoreCase))
{
var manifestPath = Path.Combine(directory, "package.json");
if (!File.Exists(manifestPath)) if (!File.Exists(manifestPath))
{ {
throw new FileNotFoundException($"Required package manifest is missing: {expected.Directory}", manifestPath); throw new FileNotFoundException("Declared package submodule has no package.json.", manifestPath);
} }
var manifest = JObject.Parse(File.ReadAllText(manifestPath)); var manifest = JObject.Parse(File.ReadAllText(manifestPath));
var packageName = manifest.Value<string>("name"); var packageName = manifest.Value<string>("name");
var version = manifest.Value<string>("version"); var version = manifest.Value<string>("version");
if (packageName == null || version == null || packageName.Length == 0 || version.Length == 0) if (string.IsNullOrWhiteSpace(packageName) || string.IsNullOrWhiteSpace(version))
{ {
throw new InvalidOperationException($"{expected.Directory} has no valid package name or version."); throw new InvalidOperationException($"{directory} has no valid package name or version.");
} }
if (!string.Equals(packageName, expected.Name, StringComparison.Ordinal) || var validPackageName = packageName!;
!string.Equals(version, expected.Version, StringComparison.Ordinal)) var validVersion = version!;
if (!SemanticVersion.IsMatch(validVersion))
{ {
throw new InvalidOperationException( throw new InvalidOperationException($"{validPackageName} has an invalid semantic version: {validVersion}");
$"{expected.Directory} must be {expected.Name}@{expected.Version}, found {packageName}@{version}.");
} }
if (result.ContainsKey(packageName)) if (result.ContainsKey(validPackageName))
{ {
throw new InvalidOperationException($"Duplicate package manifest name: {packageName}"); throw new InvalidOperationException($"Duplicate package manifest name: {validPackageName}");
} }
result.Add(packageName, new PackageDefinition(packageName, version, manifest)); result.Add(validPackageName, new PackageDefinition(validPackageName, validVersion, manifest));
}
return result;
}
private static HashSet<string> ReadDeclaredPackageDirectories(string workspaceRoot)
{
var gitModulesPath = Path.Combine(workspaceRoot, ".gitmodules");
if (!File.Exists(gitModulesPath))
{
throw new FileNotFoundException("Workspace .gitmodules is missing.", gitModulesPath);
}
var result = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var line in File.ReadLines(gitModulesPath))
{
var match = Regex.Match(line, @"^\s*path\s*=\s*(?<path>.+?)\s*$", RegexOptions.CultureInvariant);
if (!match.Success)
{
continue;
}
var relativePath = match.Groups["path"].Value.Replace('\\', '/');
if (!relativePath.StartsWith("Assets/Modules/", StringComparison.Ordinal) ||
relativePath.Substring("Assets/Modules/".Length).Contains("/"))
{
continue;
}
var fullPath = Path.GetFullPath(Path.Combine(workspaceRoot, relativePath));
if (!result.Add(fullPath))
{
throw new InvalidOperationException($"Duplicate package submodule path: {relativePath}");
}
}
if (result.Count == 0)
{
throw new InvalidOperationException("No Assets/Modules package submodules are declared.");
} }
return result; return result;
@@ -158,6 +204,63 @@ namespace ShrinkSDK.WorkspaceValidation
} }
} }
private static void ValidateInstallerCatalog(IReadOnlyDictionary<string, PackageDefinition> packages)
{
var catalogType = AppDomain.CurrentDomain.GetAssemblies()
.Select(assembly => assembly.GetType(InstallerCatalogTypeName, false))
.FirstOrDefault(type => type != null)
?? throw new InvalidOperationException($"Installer catalog type was not loaded: {InstallerCatalogTypeName}");
var packagesField = catalogType.GetField("Packages", BindingFlags.Static | BindingFlags.NonPublic)
?? throw new InvalidOperationException("Installer catalog Packages field was not found.");
var catalogItems = packagesField.GetValue(null) as IEnumerable
?? throw new InvalidOperationException("Installer catalog Packages field is not enumerable.");
var catalog = new Dictionary<string, string>(StringComparer.Ordinal);
foreach (var item in catalogItems)
{
if (item == null)
{
throw new InvalidOperationException("Installer catalog contains a null package entry.");
}
var itemType = item.GetType();
var packageName = itemType.GetProperty("PackageName")?.GetValue(item) as string;
var version = itemType.GetProperty("Version")?.GetValue(item) as string;
if (string.IsNullOrWhiteSpace(packageName) || string.IsNullOrWhiteSpace(version))
{
throw new InvalidOperationException("Installer catalog contains an invalid package entry.");
}
var validPackageName = packageName!;
var validVersion = version!;
if (!catalog.TryAdd(validPackageName, validVersion))
{
throw new InvalidOperationException($"Installer catalog contains duplicate package: {validPackageName}");
}
}
var expectedNames = packages.Keys
.Where(name => !string.Equals(name, InstallerPackageName, StringComparison.Ordinal))
.ToHashSet(StringComparer.Ordinal);
var missing = expectedNames.Except(catalog.Keys).OrderBy(name => name, StringComparer.Ordinal).ToArray();
var unexpected = catalog.Keys.Except(expectedNames).OrderBy(name => name, StringComparer.Ordinal).ToArray();
if (missing.Length > 0 || unexpected.Length > 0)
{
throw new InvalidOperationException(
$"Installer catalog package set differs from Workspace. Missing=[{string.Join(", ", missing)}], " +
$"unexpected=[{string.Join(", ", unexpected)}]");
}
foreach (var packageName in expectedNames)
{
if (!string.Equals(catalog[packageName], packages[packageName].Version, StringComparison.Ordinal))
{
throw new InvalidOperationException(
$"Installer catalog requires {packageName} {catalog[packageName]}, but the Workspace provides {packages[packageName].Version}.");
}
}
}
private sealed class PackageDefinition private sealed class PackageDefinition
{ {
public PackageDefinition(string name, string version, JObject manifest) public PackageDefinition(string name, string version, JObject manifest)
@@ -171,20 +274,6 @@ namespace ShrinkSDK.WorkspaceValidation
public string Version { get; } public string Version { get; }
public JObject Manifest { get; } public JObject Manifest { get; }
} }
private readonly struct ExpectedPackage
{
public ExpectedPackage(string directory, string name, string version)
{
Directory = directory;
Name = name;
Version = version;
}
public string Directory { get; }
public string Name { get; }
public string Version { get; }
}
} }
} }
#endif #endif
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 10936b7d408f403449df0946277a2d00
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+30 -26
View File
@@ -65,7 +65,7 @@ ShrinkSDK 是以 Unity Package Manager 包为发布边界的 SDK Workspace,不
```text ```text
ShrinkSDK Workspace/ ShrinkSDK Workspace/
|-- Assets/Modules/ 21 个同路径 Git submodule UPM 包 |-- Assets/Modules/ .gitmodules 声明的同路径 Git submodule UPM 包
|-- Assets/Modules/*.meta 根仓库追踪的 Unity 目录 GUID |-- Assets/Modules/*.meta 根仓库追踪的 Unity 目录 GUID
|-- Assets/Scenes/ 示例与验收场景 |-- Assets/Scenes/ 示例与验收场景
|-- Assets/Resources/ 当前应用配置与组合 Profile |-- Assets/Resources/ 当前应用配置与组合 Profile
@@ -73,6 +73,7 @@ ShrinkSDK Workspace/
|-- GeneratedModSdk/ Mod SDK 导出物 |-- GeneratedModSdk/ Mod SDK 导出物
|-- Packages/ 根 Unity 工程依赖 |-- Packages/ 根 Unity 工程依赖
|-- Tools/UpmConsumerValidation/ 干净 UPM 消费工程验证 |-- Tools/UpmConsumerValidation/ 干净 UPM 消费工程验证
|-- Tools/Release/ 包版本、依赖与目录同步工具
|-- Tools/RepositoryMigration/ 子模块、发布仓库与独立宿主初始化脚本 |-- Tools/RepositoryMigration/ 子模块、发布仓库与独立宿主初始化脚本
|-- Docs/Archive/ 已完成迁移与过期地图,仅供追溯 |-- Docs/Archive/ 已完成迁移与过期地图,仅供追溯
|-- DESIGN.md 唯一当前架构文档 |-- DESIGN.md 唯一当前架构文档
@@ -84,27 +85,28 @@ ShrinkSDK Workspace/
| 包 | 版本 | 职责 | | 包 | 版本 | 职责 |
|---|---:|---| |---|---:|---|
| `com.cneicy.shrink-eventbus` | 2.0.0 | 单一事件模型、多 Bus、生成特性订阅、UniTask 调度与零 GC 热路径 | | `com.cneicy.shrink-eventbus` | 2.1.0 | 单一事件模型、多 Bus、生成特性订阅、可选 MonoBehaviour 生命周期织入、UniTask 调度与零 GC 热路径 |
| `com.cneicy.shrink-eventbus-entities` | 0.1.0 | ShrinkEventBus 的 ECS/Burst NativeQueue writer 与 playback 适配 | | `com.cneicy.shrink-eventbus-entities` | 0.1.2 | ShrinkEventBus 的 ECS/Burst NativeQueue writer 与 playback 适配 |
| `com.cneicy.shrink-datasaver` | 2.2.0 | 多槽位存档、设置、迁移、加密、原子写入与备份 | | `com.cneicy.shrink-datasaver` | 2.3.0 | 多槽位存档、设置、迁移、加密、原子写入与备份 |
| `com.cneicy.shrink-command` | 0.2.0 | 路径式命令、权限与同步/异步执行 | | `com.cneicy.shrink-command` | 0.3.0 | 路径式命令、权限与同步/异步执行 |
| `com.cneicy.shrink-network` | 0.2.0 | 消息、RPC、权限、诊断、TCP/KCP/Loopback 与服务器生成 | | `com.cneicy.shrink-network` | 0.3.0 | 消息、RPC、权限、诊断、TCP/KCP/Loopback 与服务器生成 |
| `com.cneicy.shrink-mod-framework` | 0.2.1 | 模组发现、依赖、可逆生命周期、命名空间内容覆盖、外部 DLL revision 与 Harmony lease | | `com.cneicy.shrink-mod-framework` | 0.3.0 | 模组发现、依赖、可逆生命周期、命名空间内容覆盖、外部 DLL revision 与 Harmony lease |
| `com.cneicy.shrink-tutorial` | 0.1.0 | 数据驱动引导、遮罩、锚点、触发与持久化 | | `com.cneicy.shrink-tutorial` | 0.2.0 | 数据驱动引导、遮罩、锚点、触发与持久化 |
| `com.cneicy.shrink-context-core` | 0.1.0 | 可逆效应、coeffect、fiber、声明式 loader 与诊断 | | `com.cneicy.shrink-context-core` | 0.2.0 | 可逆效应、coeffect、fiber、声明式 loader 与诊断 |
| `com.cneicy.shrink-app-core` | 0.1.1 | App 设置、服务门面、ClassicHost 兼容面与宿主协议 | | `com.cneicy.shrink-app-core` | 0.2.0 | App 设置、服务门面、ClassicHost 兼容面与宿主协议 |
| `com.cneicy.shrink-app-starter-basic` | 0.1.0 | 默认 Context 组合根、配置资产和示例入口 | | `com.cneicy.shrink-app-starter-basic` | 0.3.0 | 默认 Context 组合根、配置资产和示例入口 |
| `com.cneicy.shrink-context-app-adapter` | 0.1.0 | ContextLoader 宿主、Profile/JSON、诊断与基准 | | `com.cneicy.shrink-context-app-adapter` | 0.2.0 | ContextLoader 宿主、Profile/JSON、诊断与基准 |
| `com.cneicy.shrink-context-eventbus-adapter` | 0.1.0 | EventBus 生成绑定的可逆 EffectAttach 包装 | | `com.cneicy.shrink-context-eventbus-adapter` | 0.2.0 | EventBus 生成绑定的可逆 EffectAttach 包装 |
| `com.cneicy.shrink-datasaver-integration-eventbus` | 2.1.0 | DataSaver 事件桥 | | `com.cneicy.shrink-datasaver-integration-eventbus` | 2.2.0 | DataSaver 事件桥 |
| `com.cneicy.shrink-datasaver-integration-app` | 0.1.0 | DataSaver App installer/原生 Context 组件 | | `com.cneicy.shrink-datasaver-integration-app` | 0.2.0 | DataSaver App installer/原生 Context 组件 |
| `com.cneicy.shrink-command-integration-eventbus` | 0.1.1 | 命令请求与生命周期事件桥 | | `com.cneicy.shrink-command-integration-eventbus` | 0.2.0 | 命令请求与生命周期事件桥 |
| `com.cneicy.shrink-command-integration-network` | 0.1.0 | `command/execute` RPC 桥 | | `com.cneicy.shrink-command-integration-network` | 0.2.0 | `command/execute` RPC 桥 |
| `com.cneicy.shrink-command-integration-app` | 0.1.0 | Command App installer/原生 Context 组件 | | `com.cneicy.shrink-command-integration-app` | 0.2.0 | Command App installer/原生 Context 组件 |
| `com.cneicy.shrink-network-integration-eventbus` | 0.1.1 | 网络事件广播、裁决结果与 delta 去重 | | `com.cneicy.shrink-network-integration-eventbus` | 0.2.0 | 网络事件广播、裁决结果与 delta 去重 |
| `com.cneicy.shrink-network-integration-app` | 0.1.0 | Network App installer/原生 Context 组件 | | `com.cneicy.shrink-network-integration-app` | 0.2.0 | Network App installer/原生 Context 组件 |
| `com.cneicy.shrink-shared-codegen` | 0.1.0 | App、Command、Network 共用的 Editor-only IL 后处理注册表生成器 | | `com.cneicy.shrink-shared-codegen` | 0.1.1 | App、Command、Network 共用的 Editor-only IL 后处理注册表生成器 |
| `com.cneicy.shrink-installer` | 0.1.2 | 安全合并公开 registry、显示诊断并固定版本安装 Starter 或选定模块的 Editor 引导包 | | `com.cneicy.shrink-runtime-abstractions` | 0.1.0 | Unity 与其他 .NET 宿主共用的平台服务合同 |
| `com.cneicy.shrink-installer` | 0.2.6 | UI Toolkit 包管理器:显示真实安装状态,按 Context 分层提供固定版本单包与推荐组合 |
`ShrinkShared.CodeGen` 不反向引用业务 asmdef,只按程序集名与类型全名读取 Cecil 元数据,因此业务包可以依赖它而不形成包循环。 `ShrinkShared.CodeGen` 不反向引用业务 asmdef,只按程序集名与类型全名读取 Cecil 元数据,因此业务包可以依赖它而不形成包循环。
@@ -157,7 +159,7 @@ Cordis 迁移的阶段 0 至阶段 5 已进入当前架构,不再是待办计
`ShrinkAppLoaderBootstrapper.DefaultComposition` 接入 Basic Starter 的七个声明式模块:Command、DataSaver、Network 三个原生组件,Command-Network 集成,以及 Command/DataSaver/Network 的 EventBus 集成。 `ShrinkAppLoaderBootstrapper.DefaultComposition` 接入 Basic Starter 的七个声明式模块:Command、DataSaver、Network 三个原生组件,Command-Network 集成,以及 Command/DataSaver/Network 的 EventBus 集成。
`ShrinkAppCompositionProfile` 用 ScriptableObject/JSON 决定条目启用、显式排除、isolate 与 intercept;组件工厂仍由代码注册,配置文件不能按任意类型名反射实例化对象。`ShrinkSDK/Cordis/诊断与组合` 显示 fiber、等待依赖、provider target、事务和外部程序集 revision。 `ShrinkAppCompositionProfile` 用 ScriptableObject/JSON 决定条目启用、显式排除、isolate 与 intercept;组件工厂仍由代码注册,配置文件不能按任意类型名反射实例化对象。`ShrinkSDK/上下文/诊断与组合` 显示 fiber、等待依赖、provider target、事务和外部程序集 revision。
`ShrinkApp.IsRunning``ShrinkApp.Services` 已接回 LoaderHost。原生组件停用时使用 `TryUnregister(instance)` 撤回对应门面。旧 `IShrinkAppModuleInstaller` 适配仍可用于 ClassicHost,但默认 ContextLoader 不再通过它启动核心模块。 `ShrinkApp.IsRunning``ShrinkApp.Services` 已接回 LoaderHost。原生组件停用时使用 `TryUnregister(instance)` 撤回对应门面。旧 `IShrinkAppModuleInstaller` 适配仍可用于 ClassicHost,但默认 ContextLoader 不再通过它启动核心模块。
@@ -206,8 +208,8 @@ Mono 中已加载程序集不能真正卸载。系统只回滚组件实例与效
菜单入口: 菜单入口:
- `ShrinkSDK/Network/生成完整独立服务器工程` - `ShrinkSDK/网络/生成完整独立服务器工程`
- `ShrinkSDK/Network/刷新独立服务器 Generated 合同` - `ShrinkSDK/网络/刷新独立服务器生成合同`
完整生成分两步: 完整生成分两步:
@@ -225,7 +227,7 @@ Mono 中已加载程序集不能真正卸载。系统只回滚组件实例与效
1. 内部包图检查:全部 `com.cneicy.*` 依赖版本一致、无循环,普通主包不反向依赖 Integration 包。 1. 内部包图检查:全部 `com.cneicy.*` 依赖版本一致、无循环,普通主包不反向依赖 Integration 包。
2. Unity 编译:目标 asmdef 和根项目无编译错误。 2. Unity 编译:目标 asmdef 和根项目无编译错误。
3. EditMode 测试:功能测试、Context 生命周期、语义扫描和模板覆写保护。 3. EditMode 测试:功能测试、Context 生命周期、语义扫描和模板覆写保护。
4. 真实 UPM 消费:`Tools/UpmConsumerValidation/Validate-UpmConsumer.ps1` 在递归 submodule 初始化后的 Workspace 中创建仓库外形态的临时 Unity 工程,通过 `file:` 安装全部 21 个包,启用 testables,验证包注册、程序集加载并运行 EditMode 测试。发布后还必须分别验证 registry、精确 Git URL 和 Installer 三种空白消费者工程路径。 4. 真实 UPM 消费:`Tools/UpmConsumerValidation/Validate-UpmConsumer.ps1` 在递归 submodule 初始化后的 Workspace 中创建仓库外形态的临时 Unity 工程,通过 `file:` 安装全部包,启用 testables,验证包注册、程序集加载并运行 EditMode 测试。发布后还必须分别验证 registry、精确 Git URL 和 Installer 三种空白消费者工程路径。
5. 独立宿主:生成工程与 RuntimeSmoke 按变更范围构建或运行。 5. 独立宿主:生成工程与 RuntimeSmoke 按变更范围构建或运行。
6. 涉及真实生命周期时,仍需在目标场景执行 Play Mode 验收;源码检查和 EditMode 不能替代该路径。 6. 涉及真实生命周期时,仍需在目标场景执行 Play Mode 验收;源码检查和 EditMode 不能替代该路径。
@@ -253,6 +255,8 @@ Mono 中已加载程序集不能真正卸载。系统只回滚组件实例与效
- 当前架构只更新本文。 - 当前架构只更新本文。
- 包级使用方式和 API 示例放在各包 README。 - 包级使用方式和 API 示例放在各包 README。
- `Tools/Release/Update-ShrinkSdkVersions.ps1` 负责包版本递增,并同步受影响的精确依赖、对应 NuGet 项目、Installer 目录、本文版本表和 `Tools/Release/package-versions.json`;依赖清单变化会触发依赖方 patch 版本递增。
- `Tools/UpmConsumerValidation/published-upm-versions.json` 只记录已经发布且要接受真实消费验证的版本,不随未打标签的 Workspace 版本提前更新。
- 包源码、标签、独立开发宿主与包级 CI 位于 `https://git.crash.work/ShrinkSDK/<Package>`;发布版本只能由与 `package.json.version` 一致的 `vX.Y.Z` 标签产生。 - 包源码、标签、独立开发宿主与包级 CI 位于 `https://git.crash.work/ShrinkSDK/<Package>`;发布版本只能由与 `package.json.version` 一致的 `vX.Y.Z` 标签产生。
- `NETWORK_PITFALLS.md` 记录实现经验,不描述当前模块清单。 - `NETWORK_PITFALLS.md` 记录实现经验,不描述当前模块清单。
- `Docs/Archive/CORDIS_MIGRATION.completed.md` 保存迁移论证、阶段记录和历史验收数据。 - `Docs/Archive/CORDIS_MIGRATION.completed.md` 保存迁移论证、阶段记录和历史验收数据。
+7
View File
@@ -0,0 +1,7 @@
**/bin/
**/obj/
**/.godot/
Artifacts/
Exports/
!*.csproj
!*.sln
+13
View File
@@ -0,0 +1,13 @@
<Project>
<PropertyGroup>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<Deterministic>true</Deterministic>
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
<RepositoryUrl>https://git.crash.work/ShrinkSDK/Workspace</RepositoryUrl>
<Authors>ShrinkSDK</Authors>
<Company>ShrinkSDK</Company>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>
</Project>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="Godot Local" value="C:\Users\im\Documents\godothub\engine\4.6.3-stable-mono\GodotSharp\Tools\nupkgs" />
<add key="ShrinkSDK Local" value="Artifacts" />
<add key="ShrinkSDK" value="https://git.crash.work/api/packages/ShrinkSDK/nuget/index.json" />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
</packageSources>
</configuration>
+170
View File
@@ -0,0 +1,170 @@
# ShrinkSDK for Godot 4.6 C#
Godot 适配层使用共享 ShrinkSDK 运行时和 MSBuild/Cecil CodeGen。首期目标是 Godot 4.6.3 Mono、.NET 8,以及 Windows、Linux、macOS 桌面项目;不包含 Web、Android 和 iOS。
## 直接运行 Workspace 示例
使用 Godot 4.6.3 Mono 打开:
```text
Godot/Samples/ShrinkSDK.Godot.Sample/project.godot
```
首次打开前可以先完成导入和编译:
```powershell
$godot = Join-Path $env:GODOT_HOME "godot_console.exe"
& $godot --headless --editor --path .\Godot\Samples\ShrinkSDK.Godot.Sample --quit
dotnet build .\Godot\Samples\ShrinkSDK.Godot.Sample\ShrinkSDK.Godot.Sample.csproj
& $godot --headless --path .\Godot\Samples\ShrinkSDK.Godot.Sample
```
`GODOT_HOME` 指向 Godot Mono 安装目录。成功运行时会输出 `SHRINK_GODOT_SMOKE_PASS`
## 在现有 Godot C# 项目中安装
项目需要:
- Godot 4.6.3 Mono
- .NET 8 SDK
- `.csproj` 使用 `Godot.NET.Sdk/4.6.3`
- 项目级 `NuGet.Config` 保留既有源并加入 ShrinkSDK feed。
`NuGet.Config` 的最小增量如下:
```xml
<configuration>
<packageSources>
<add key="ShrinkSDK" value="https://git.crash.work/api/packages/ShrinkSDK/nuget/index.json" />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
</packageSources>
</configuration>
```
安装 Godot 宿主和所需模块:
```powershell
dotnet add package ShrinkSDK.Godot --version 0.1.0
dotnet add package ShrinkSDK.EventBus --version 2.1.0
dotnet add package ShrinkSDK.Command --version 0.3.0
dotnet add package ShrinkSDK.Network --version 0.3.0
dotnet add package ShrinkSDK.DataSaver --version 2.3.0
dotnet restore
dotnet build
```
只有实际使用的模块需要安装。版本以 ShrinkSDK NuGet 源中的已发布版本为准,并保留命令中的固定版本号,不要改用浮动版本。
## Installer 插件
`ShrinkSDK.Godot.Installer` 的 nupkg 内包含完整的 `addons/shrinksdk`。取得 `0.1.3` 包后,将包内:
```text
contentFiles/any/any/addons/shrinksdk
```
复制到 Godot 项目的:
```text
addons/shrinksdk
```
然后在 Godot 中打开 `Project > Project Settings > Plugins`,启用 **ShrinkSDK Installer**。左侧 ShrinkSDK 面板可以:
- 查看项目直接安装的 ShrinkSDK NuGet 包及版本;
- 安装、升级或移除模块;
- 保留项目已有 NuGet 源和无关 `PackageReference`
- 执行 `dotnet restore/build`
- 查看 CodeGen 最后一次织入结果和生成的注册数量。
已安装版本高于面板目录版本时会显示 `Newer` 并禁用更新按钮,不会执行降级。
在 Workspace 中生成 Installer 包:
```powershell
dotnet build .\Assets\Modules\ShrinkInstaller\Godot~\ShrinkSDK.Godot.Installer.csproj -c Release
dotnet pack .\Assets\Modules\ShrinkInstaller\Godot~\ShrinkSDK.Godot.Installer.csproj -c Release --no-build -o .\Godot\Artifacts
```
## 宿主节点
主场景根节点继承 `ShrinkGodotHost`,并在 `_Ready` 中先调用 `base._Ready()`
```csharp
using Cysharp.Threading.Tasks;
using ShrinkSDK.Godot;
public partial class GameRoot : ShrinkGodotHost
{
public override void _Ready()
{
base._Ready();
BootAsync().Forget();
}
private async UniTaskVoid BootAsync()
{
RequireWoven(GetType().Assembly);
await StartAppAsync();
}
}
```
宿主负责日志、主线程派发、时间、`user://` 持久化路径、截图、应用生命周期和网络 dispatch queue。使用 `ShrinkNetworkService` 时,通过 `AddNetworkService(service)` 交给宿主逐帧泵送,释放前调用 `RemoveNetworkService(service)`
## CodeGen 织入
EventBus、Command、Network 和 App 的特性注册由 `ShrinkSDK.CodeGen` 在构建时生成。项目正常引用这些模块后,`buildTransitive` 会默认设置:
```xml
<ShrinkCodeGenEnabled>true</ShrinkCodeGenEnabled>
```
执行:
```powershell
dotnet build
```
日志应出现类似:
```text
[ShrinkSDK.CodeGen] ... woven: instance=1, static=1, registry=2
```
运行时可以在启动阶段验证程序集:
```csharp
ShrinkGodotHost.RequireWoven(GetType().Assembly);
```
缺少织入时应修复 NuGet 引用或构建配置,不要增加运行时反射扫描兜底。只有明确采用手工注册的普通 .NET 宿主才设置 `<ShrinkCodeGenEnabled>false</ShrinkCodeGenEnabled>`
## 特性声明示例
```csharp
using ShrinkCommand;
using ShrinkEventBus;
using ShrinkNetwork;
public readonly struct PlayerReadyEvent : IShrinkEvent { }
[ShrinkEventSubscriber]
public sealed class PlayerReadySubscriber
{
[ShrinkSubscribe]
private void OnPlayerReady(PlayerReadyEvent value) { }
}
[ShrinkCommandSubscriber]
public static class GameCommands
{
[ShrinkCommand("game/status")]
public static void Status() { }
}
[ShrinkNetworkMessage(41001, "game/player-ready")]
public sealed class PlayerReadyMessage : IShrinkNetworkMessage { }
```
实例订阅者使用 `EventBus.Attach(instance)` 绑定;静态订阅、Command、Network 和 App registry 由织入代码自动注册。
@@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3]
[ext_resource path="res://SampleRoot.cs" type="Script" id="1"]
[node name="SampleRoot" type="Node"]
script = ExtResource("1")
@@ -0,0 +1,124 @@
#nullable enable
using System;
using System.Linq;
using System.Collections.Generic;
using Godot;
using ShrinkApp;
using ShrinkApp.Starter.Basic;
using ShrinkCommand;
using Cysharp.Threading.Tasks;
using ShrinkDataSaver;
using ShrinkEventBus;
using ShrinkNetwork;
using ShrinkSDK.Godot;
using ShrinkTutorial;
public partial class SampleRoot : ShrinkGodotHost
{
public override void _Ready()
{
base._Ready();
RunSmokeAsync().Forget();
}
private async UniTaskVoid RunSmokeAsync()
{
var marker = RequireWoven(GetType().Assembly);
using var binding = EventBus.Attach(new GodotSubscriber());
EventBus.Post(new GodotSmokeEvent());
var assembly = GetType().Assembly;
var registries = assembly.GetCustomAttributes(false).Select(value => value.GetType().Name).ToHashSet();
var savedValue = 42;
ShrinkDataSaverRuntime.Initialize(new ShrinkDataSaverRuntimeConfig
{
RootPath = PersistentDataPath,
CurrentSaveVersion = 1
});
ShrinkSave.RegisterModule("godot-smoke", () => savedValue, value => savedValue = value);
await ShrinkSave.SaveSlotAsync(0, new SaveOptions { SlotName = "Godot Smoke" });
savedValue = 0;
await ShrinkSave.LoadSlotAsync(0);
var app = await StartAppAsync();
var composition = ShrinkBasicComposition.CreateHost(app.Context.Services);
var entries = ShrinkBasicComposition.CreateEntries(app.Context.Services);
await composition.ApplyAsync(entries);
await composition.ApplyAsync(entries);
var activeFibers = composition.Runtime.Fibers.Count(fiber => fiber.State == ShrinkContext.ShrinkFiberState.Active);
var tutorialStorage = new MemoryTutorialStorage();
var tutorial = new ShrinkTutorialRunner(tutorialStorage);
var tutorialStarted = tutorial.Start(new ShrinkTutorialData
{
TutorialId = "godot-smoke",
Steps = { new ShrinkTutorialStep { StepId = "one", CompleteCondition = ShrinkTutorialCompleteCondition.AnyClick } }
});
var tutorialCompleted = tutorial.CompleteStep();
if (GodotSubscriber.Calls != 1 || GodotStaticSubscriber.Calls != 1 || savedValue != 42 ||
!app.IsRunning || !GodotInstaller.Initialized ||
activeFibers != 3 || !tutorialStarted || !tutorialCompleted || !tutorialStorage.Saved ||
!registries.Contains(nameof(ShrinkCommandStaticRegistryAttribute)) ||
!registries.Contains(nameof(ShrinkNetworkMessageRegistryAttribute)))
{
GD.PushError("ShrinkSDK Godot smoke failed.");
GetTree().Quit(1);
return;
}
await composition.ShutdownAsync();
var smokePath = System.IO.Path.Combine(OS.GetUserDataDir(), "shrink_smoke_pass.txt");
System.IO.File.WriteAllText(smokePath, $"CodeGen={marker.WeaverVersion};DataSaver={savedValue}");
if (!System.IO.File.Exists(smokePath))
throw new System.IO.IOException($"Failed to create smoke sentinel: {smokePath}");
GD.Print($"SHRINK_GODOT_SMOKE_PASS CodeGen={marker.WeaverVersion} DataSaver={savedValue}");
GetTree().Quit(0);
}
}
public sealed class MemoryTutorialStorage : IShrinkTutorialStorage
{
private ShrinkTutorialProgress _progress = new();
public bool Saved { get; private set; }
public ShrinkTutorialProgress Load() => _progress;
public void Save(ShrinkTutorialProgress progress) { _progress = progress; Saved = true; }
public void Reset() { _progress = new ShrinkTutorialProgress(); Saved = false; }
}
public readonly struct GodotSmokeEvent : IShrinkEvent { }
[ShrinkEventSubscriber]
public sealed class GodotSubscriber
{
public static int Calls;
[ShrinkSubscribe] private void Handle(GodotSmokeEvent value) => Calls++;
}
[ShrinkEventSubscriber]
public static class GodotStaticSubscriber
{
public static int Calls;
[ShrinkSubscribe] private static void Handle(GodotSmokeEvent value) => Calls++;
}
[ShrinkCommandSubscriber]
public static class GodotCommands
{
[ShrinkCommand("godot/smoke")] public static void Smoke() { }
}
[ShrinkNetworkMessage(42001, "godot/smoke")]
public sealed class GodotSmokeMessage : IShrinkNetworkMessage { }
[ShrinkAppModuleInstaller]
public sealed class GodotInstaller : IShrinkAppModuleInstaller
{
public static bool Initialized;
public string ModuleId => "godot-smoke";
public int Order => 0;
public IReadOnlyList<string> DependsOn => Array.Empty<string>();
public void RegisterServices(ShrinkAppContext context) => context.Services.Register(this);
public UniTask InitializeAsync(ShrinkAppContext context)
{
Initialized = true;
return UniTask.CompletedTask;
}
}
@@ -0,0 +1 @@
uid://j565ln7hmg6i
@@ -0,0 +1,20 @@
<Project Sdk="Godot.NET.Sdk/4.6.3">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<EnableDynamicLoading>true</EnableDynamicLoading>
<ShrinkCodeGenEnabled>true</ShrinkCodeGenEnabled>
<_ShrinkCodeGenTaskAssembly>$(MSBuildThisFileDirectory)..\..\..\Assets\Modules\ShrinkShared.CodeGen\DotNet~\ShrinkSDK.CodeGen.Task\bin\$(Configuration)\net8.0\ShrinkSDK.CodeGen.Task.dll</_ShrinkCodeGenTaskAssembly>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkRuntime.Abstractions\DotNet~\ShrinkSDK.Runtime.Abstractions.csproj" />
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkEventBus\DotNet~\ShrinkSDK.EventBus.csproj" />
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkCommand\DotNet~\ShrinkSDK.Command.csproj" />
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkNetwork\DotNet~\ShrinkSDK.Network.csproj" />
<ProjectReference Include="..\..\Packages\ShrinkSDK.Godot\ShrinkSDK.Godot.csproj" />
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkApp.Starter.Basic\DotNet~\ShrinkSDK.App.Starter.Basic.csproj" />
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkTutorial\DotNet~\ShrinkSDK.Tutorial.csproj" />
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkTutorial\Godot~\ShrinkSDK.Tutorial.Godot.csproj" />
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkShared.CodeGen\DotNet~\ShrinkSDK.CodeGen.Task\ShrinkSDK.CodeGen.Task.csproj" ReferenceOutputAssembly="false" />
</ItemGroup>
<Import Project="..\..\..\Assets\Modules\ShrinkShared.CodeGen\DotNet~\ShrinkSDK.CodeGen.Task\buildTransitive\ShrinkSDK.CodeGen.targets" />
</Project>
@@ -0,0 +1,24 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ShrinkSDK.Godot.Sample", "ShrinkSDK.Godot.Sample.csproj", "{8ED6C49D-909C-4A5A-B80C-3537D1349377}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
ExportDebug|Any CPU = ExportDebug|Any CPU
ExportRelease|Any CPU = ExportRelease|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{8ED6C49D-909C-4A5A-B80C-3537D1349377}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8ED6C49D-909C-4A5A-B80C-3537D1349377}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8ED6C49D-909C-4A5A-B80C-3537D1349377}.ExportDebug|Any CPU.ActiveCfg = ExportDebug|Any CPU
{8ED6C49D-909C-4A5A-B80C-3537D1349377}.ExportDebug|Any CPU.Build.0 = ExportDebug|Any CPU
{8ED6C49D-909C-4A5A-B80C-3537D1349377}.ExportRelease|Any CPU.ActiveCfg = ExportRelease|Any CPU
{8ED6C49D-909C-4A5A-B80C-3537D1349377}.ExportRelease|Any CPU.Build.0 = ExportRelease|Any CPU
{8ED6C49D-909C-4A5A-B80C-3537D1349377}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8ED6C49D-909C-4A5A-B80C-3537D1349377}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
@@ -0,0 +1,12 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://TutorialDemo.cs" id="1"]
[node name="TutorialDemo" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1")
@@ -0,0 +1,49 @@
#nullable enable
using Godot;
using ShrinkTutorial;
using ShrinkTutorial.Godot;
public partial class TutorialDemo : Control
{
public override void _Ready()
{
var target = new Button
{
Text = "Tutorial Target",
Position = new Vector2(480, 260),
CustomMinimumSize = new Vector2(220, 72)
};
target.AddToGroup("shrink_tutorial_primary");
AddChild(target);
var overlay = new ShrinkGodotTutorialOverlay();
AddChild(overlay);
var runner = new ShrinkTutorialRunner(new MemoryTutorialStorage());
runner.Start(new ShrinkTutorialData
{
TutorialId = "visual-smoke",
Steps =
{
new ShrinkTutorialStep
{
StepId = "target",
Title = "Godot Tutorial",
Body = "Target hole, dialog and input blocker smoke test",
TargetMode = ShrinkTutorialTargetMode.AnchorId,
Target = "primary",
CompleteCondition = ShrinkTutorialCompleteCondition.ClickTarget
}
}
});
overlay.Bind(runner);
GetTree().CreateTimer(2).Timeout += QuitCleanly;
}
private void QuitCleanly()
{
var tree = GetTree();
tree.UnloadCurrentScene();
tree.CreateTimer(0.1).Timeout += () => tree.Quit(0);
}
}
@@ -0,0 +1 @@
uid://bhjfn5prdv2pw
@@ -0,0 +1,52 @@
[preset.0]
name="Windows Desktop"
platform="Windows Desktop"
runnable=true
dedicated_server=false
custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path="../../Exports/Windows/ShrinkSDKGodot.exe"
script_export_mode=2
[preset.0.options]
binary_format/architecture="x86_64"
debug/export_console_wrapper=1
[preset.1]
name="Linux"
platform="Linux"
runnable=false
dedicated_server=false
custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path="../../Exports/Linux/ShrinkSDKGodot.x86_64"
script_export_mode=2
[preset.1.options]
binary_format/architecture="x86_64"
[preset.2]
name="macOS"
platform="macOS"
runnable=false
dedicated_server=false
custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path="../../Exports/macOS/ShrinkSDKGodot.zip"
script_export_mode=2
[preset.2.options]
application/bundle_identifier="work.crash.shrinksdk.sample"
binary_format/architecture="universal"
@@ -0,0 +1,30 @@
; Engine configuration file.
; It's best edited using the editor UI and not directly,
; since the parameters that go here are not all obvious.
;
; Format:
; [section] ; section goes between []
; param=value ; assign values to parameters
config_version=5
[application]
config/name="ShrinkSDK Godot Sample"
run/main_scene="res://Main.tscn"
config/features=PackedStringArray("4.6", "C#")
[display]
window/size/viewport_width=960
window/size/viewport_height=540
[dotnet]
project/assembly_name="ShrinkSDK.Godot.Sample"
[rendering]
renderer/rendering_method="gl_compatibility"
renderer/rendering_method.mobile="gl_compatibility"
textures/vram_compression/import_etc2_astc=true
+45
View File
@@ -0,0 +1,45 @@
<Solution>
<Folder Name="/Packages/">
<Project Path="../Assets/Modules/ShrinkRuntime.Abstractions/DotNet~/ShrinkSDK.Runtime.Abstractions.csproj" />
<Project Path="../Assets/Modules/ShrinkContext.Core/DotNet~/ShrinkSDK.Context.Core.csproj" />
<Project Path="../Assets/Modules/ShrinkEventBus/DotNet~/ShrinkSDK.EventBus.csproj" />
<Project Path="../Assets/Modules/ShrinkCommand/DotNet~/ShrinkSDK.Command.csproj" />
<Project Path="../Assets/Modules/ShrinkNetwork/DotNet~/ShrinkSDK.Network.csproj" />
<Project Path="../Assets/Modules/ShrinkCommand.Integration.EventBus/DotNet~/ShrinkSDK.Command.Integration.EventBus.csproj" />
<Project Path="../Assets/Modules/ShrinkCommand.Integration.Network/DotNet~/ShrinkSDK.Command.Integration.Network.csproj" />
<Project Path="../Assets/Modules/ShrinkCommand.Integration.App/DotNet~/ShrinkSDK.Command.Integration.App.csproj" />
<Project Path="../Assets/Modules/ShrinkDataSaver.Integration.EventBus/DotNet~/ShrinkSDK.DataSaver.Integration.EventBus.csproj" />
<Project Path="../Assets/Modules/ShrinkDataSaver.Integration.App/DotNet~/ShrinkSDK.DataSaver.Integration.App.csproj" />
<Project Path="../Assets/Modules/ShrinkNetwork.Integration.EventBus/DotNet~/ShrinkSDK.Network.Integration.EventBus.csproj" />
<Project Path="../Assets/Modules/ShrinkNetwork.Integration.App/DotNet~/ShrinkSDK.Network.Integration.App.csproj" />
<Project Path="../Assets/Modules/ShrinkDataSaver/DotNet~/ShrinkSDK.DataSaver.csproj" />
<Project Path="../Assets/Modules/ShrinkApp.Core/DotNet~/ShrinkSDK.App.Core.csproj" />
<Project Path="../Assets/Modules/ShrinkContext.AppAdapter/DotNet~/ShrinkSDK.Context.AppAdapter.csproj" />
<Project Path="../Assets/Modules/ShrinkApp.Starter.Basic/DotNet~/ShrinkSDK.App.Starter.Basic.csproj" />
<Project Path="../Assets/Modules/ShrinkModFramework/DotNet~/ShrinkSDK.ModFramework.csproj" />
<Project Path="../Assets/Modules/ShrinkModFramework/Godot~/ShrinkSDK.ModFramework.Godot.csproj" />
<Project Path="../Assets/Modules/ShrinkTutorial/DotNet~/ShrinkSDK.Tutorial.csproj" />
<Project Path="../Assets/Modules/ShrinkTutorial/Godot~/ShrinkSDK.Tutorial.Godot.csproj" />
<Project Path="Packages/ShrinkSDK.Godot/ShrinkSDK.Godot.csproj" />
</Folder>
<Folder Name="/CodeGen/">
<Project Path="../Assets/Modules/ShrinkShared.CodeGen/DotNet~/ShrinkSDK.CodeGen.Core/ShrinkSDK.CodeGen.Core.csproj" />
<Project Path="../Assets/Modules/ShrinkShared.CodeGen/DotNet~/ShrinkSDK.CodeGen.Analyzers/ShrinkSDK.CodeGen.Analyzers.csproj" />
<Project Path="../Assets/Modules/ShrinkShared.CodeGen/DotNet~/ShrinkSDK.CodeGen.Task/ShrinkSDK.CodeGen.Task.csproj" />
</Folder>
<Folder Name="/Tests/">
<Project Path="Tests/CodeGenFixture/CodeGenFixture.csproj" />
<Project Path="Tests/NuGetConsumer/NuGetConsumer.csproj" />
<Project Path="Tests/CodeGenValidation/CodeGenValidation.csproj" />
<Project Path="Tests/UnityAdapterCompile/UnityAdapterCompile.csproj" />
<Project Path="Tests/ModFixture/ModFixture.csproj" />
<Project Path="Tests/ModHostFixture/ModHostFixture.csproj" />
<Project Path="Tests/InstallerFixture/InstallerFixture.csproj" />
</Folder>
<Folder Name="/Samples/">
<Project Path="Samples/ShrinkSDK.Godot.Sample/ShrinkSDK.Godot.Sample.csproj" />
</Folder>
<Folder Name="/Installer/">
<Project Path="../Assets/Modules/ShrinkInstaller/Godot~/ShrinkSDK.Godot.Installer.csproj" />
</Folder>
</Solution>
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<AssemblyName>ShrinkSDK.CodeGenFixture</AssemblyName>
<ShrinkCodeGenEnabled>true</ShrinkCodeGenEnabled>
<SignAssembly Condition="'$(FixtureSignAssembly)' == 'true'">true</SignAssembly>
<AssemblyOriginatorKeyFile Condition="'$(FixtureSignAssembly)' == 'true'">$(FixtureStrongNameKeyFile)</AssemblyOriginatorKeyFile>
<_ShrinkCodeGenTaskAssembly>$(MSBuildThisFileDirectory)..\..\..\Assets\Modules\ShrinkShared.CodeGen\DotNet~\ShrinkSDK.CodeGen.Task\bin\$(Configuration)\net8.0\ShrinkSDK.CodeGen.Task.dll</_ShrinkCodeGenTaskAssembly>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkRuntime.Abstractions\DotNet~\ShrinkSDK.Runtime.Abstractions.csproj" />
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkEventBus\DotNet~\ShrinkSDK.EventBus.csproj" />
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkCommand\DotNet~\ShrinkSDK.Command.csproj" />
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkNetwork\DotNet~\ShrinkSDK.Network.csproj" />
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkNetwork.Integration.EventBus\DotNet~\ShrinkSDK.Network.Integration.EventBus.csproj" />
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkShared.CodeGen\DotNet~\ShrinkSDK.CodeGen.Task\ShrinkSDK.CodeGen.Task.csproj" ReferenceOutputAssembly="false" />
</ItemGroup>
<Import Project="..\..\..\Assets\Modules\ShrinkShared.CodeGen\DotNet~\ShrinkSDK.CodeGen.Task\buildTransitive\ShrinkSDK.CodeGen.targets" />
</Project>
+79
View File
@@ -0,0 +1,79 @@
#nullable enable
using System;
using System.Linq;
using ShrinkCommand;
using ShrinkEventBus;
using ShrinkNetwork;
using ShrinkNetwork.Integration;
using ShrinkSDK.Runtime;
var assembly = typeof(Program).Assembly;
var marker = assembly.GetCustomAttributes(typeof(ShrinkCodeGenWovenAttribute), false)
.Cast<ShrinkCodeGenWovenAttribute>()
.SingleOrDefault() ?? throw new InvalidOperationException("CodeGen marker is missing.");
if (marker.WeaverVersion != "0.1.0" || !Guid.TryParse(marker.InputMvid, out _))
throw new InvalidOperationException("CodeGen marker is invalid.");
if (!typeof(InstanceSubscriber).GetInterfaces().Contains(typeof(IShrinkGeneratedSubscriber)))
throw new InvalidOperationException("Generated subscriber interface is missing.");
using var binding = EventBus.Attach(new InstanceSubscriber());
EventBus.Post(new FixtureEvent());
if (InstanceSubscriber.Calls != 1 || StaticSubscriber.Calls != 1)
throw new InvalidOperationException($"Generated EventBus bindings failed: instance={InstanceSubscriber.Calls}, static={StaticSubscriber.Calls}.");
if (assembly.GetCustomAttributes(typeof(ShrinkCommandStaticRegistryAttribute), false).Length != 1)
throw new InvalidOperationException("Command registry metadata is missing.");
if (assembly.GetCustomAttributes(typeof(ShrinkNetworkMessageRegistryAttribute), false).Length != 1)
throw new InvalidOperationException("Network message registry metadata is missing.");
if (assembly.GetCustomAttributes(typeof(ShrinkNetworkStaticSubscriberRegistryAttribute), false).Length != 1)
throw new InvalidOperationException("Network subscriber registry metadata is missing.");
var bridgeRegistrations = (System.Collections.IEnumerable)(typeof(ShrinkNetworkEventRegistry)
.GetMethod("Snapshot", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)!
.Invoke(null, null) ?? throw new InvalidOperationException("Network EventBus registry snapshot failed."));
if (!bridgeRegistrations.Cast<object>().Any(item => item.GetType().GetProperty("EventType")?.GetValue(item) as Type == typeof(FixtureNetworkEvent)))
throw new InvalidOperationException("Generated Network/EventBus binding is missing.");
Console.WriteLine($"PASS CodeGen {marker.WeaverVersion}: EventBus + Command + Network + Network/EventBus registries");
public readonly struct FixtureEvent : IShrinkEvent { }
[ShrinkEventSubscriber]
public sealed class InstanceSubscriber
{
public static int Calls;
[ShrinkSubscribe]
private void OnEvent(FixtureEvent value) => Calls++;
}
[ShrinkEventSubscriber]
public static class StaticSubscriber
{
public static int Calls;
[ShrinkSubscribe]
private static void OnEvent(FixtureEvent value) => Calls++;
}
[ShrinkCommandSubscriber]
public static class FixtureCommands
{
[ShrinkCommand("fixture/ping")]
public static void Ping() { }
}
[ShrinkNetworkMessage(41001, "fixture/ping")]
public sealed class FixtureMessage : IShrinkNetworkMessage { }
[ShrinkNetworkEvent]
[ShrinkNetworkMessage(41002, "fixture/event")]
public sealed class FixtureNetworkEvent : IShrinkEvent, IShrinkNetworkMessage { }
[ShrinkNetworkSubscriber]
public static class FixtureNetworkHandlers
{
[ShrinkNetworkSubscribe]
public static void Handle(FixtureMessage message) { }
}
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<AssemblyName>ShrinkSDK.CodeGenValidation</AssemblyName>
<ShrinkCodeGenEnabled>false</ShrinkCodeGenEnabled>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkShared.CodeGen\DotNet~\ShrinkSDK.CodeGen.Core\ShrinkSDK.CodeGen.Core.csproj" />
<PackageReference Include="Mono.Cecil" Version="0.11.6" />
</ItemGroup>
</Project>
+163
View File
@@ -0,0 +1,163 @@
#nullable enable
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Mono.Cecil;
using ShrinkSDK.CodeGen;
if (args.Length == 2 && args[0] == "--marker")
{
using var exported = AssemblyDefinition.ReadAssembly(args[1]);
var marker = exported.CustomAttributes.SingleOrDefault(attribute =>
attribute.AttributeType.FullName == "ShrinkSDK.Runtime.ShrinkCodeGenWovenAttribute");
Require(marker != null, "exported assembly has no ShrinkSDK CodeGen marker");
Require(exported.MainModule.Types.Any(type => type.FullName ==
"ShrinkEventBus.Generated.ShrinkGeneratedStaticBindings"),
"exported assembly has no static EventBus bootstrap");
Console.WriteLine($"PASS exported assembly woven by {marker!.ConstructorArguments[0].Value}");
return;
}
if ((args.Length == 3 || args.Length == 4) && args[0] == "--resign")
{
var signedAssembly = Path.GetFullPath(args[1]);
var keepOutput = args.Length == 4;
var output = keepOutput
? Path.GetFullPath(args[3])
: Path.Combine(Path.GetTempPath(), "ShrinkSDK.CodeGen.Resigned." + Guid.NewGuid().ToString("N") + ".dll");
var outputPdb = Path.ChangeExtension(output, ".pdb");
try
{
var result = ShrinkAssemblyWeaver.Weave(signedAssembly, Path.ChangeExtension(signedAssembly, ".pdb"),
BuildReferences(Path.GetDirectoryName(signedAssembly)!), output, outputPdb,
ShrinkCodeGenPlatform.EngineNeutral, args[2]);
Require(result.Succeeded && result.Changed,
"signed assembly re-weave failed: " + string.Join(" | ", result.Diagnostics.Select(item => item.Message)));
using var resigned = AssemblyDefinition.ReadAssembly(output);
Require(resigned.Name.HasPublicKey, "re-signed output has no public key");
Console.WriteLine("PASS signed assembly woven and re-signed with explicit key");
}
finally
{
if (!keepOutput)
{
if (File.Exists(output)) File.Delete(output);
if (File.Exists(outputPdb)) File.Delete(outputPdb);
}
}
return;
}
if (args.Length != 1 || !File.Exists(args[0]))
throw new ArgumentException("Pass the unwoven CodeGenFixture assembly path.");
var sourceAssembly = Path.GetFullPath(args[0]);
var sourceDirectory = Path.GetDirectoryName(sourceAssembly)!;
var sourcePdb = Path.ChangeExtension(sourceAssembly, ".pdb");
var references = BuildReferences(sourceDirectory);
var root = Path.Combine(Path.GetTempPath(), "ShrinkSDK.CodeGen.Validation", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
try
{
var woven = Path.Combine(root, "fixture.woven.dll");
var wovenPdb = Path.Combine(root, "fixture.woven.pdb");
var first = ShrinkAssemblyWeaver.Weave(sourceAssembly, sourcePdb, references, woven, wovenPdb);
Require(first.Succeeded && first.Changed,
"single weave did not change the assembly: " + string.Join(" | ", first.Diagnostics.Select(item => item.Message)));
Require(first.InstanceSubscribers == 1 && first.StaticSubscribers == 1 && first.RegistryEntries == 5,
$"unexpected generated counts: {first.InstanceSubscribers}/{first.StaticSubscribers}/{first.RegistryEntries}");
VerifyStructure(woven);
var second = ShrinkAssemblyWeaver.Weave(woven, wovenPdb, references.Append(woven),
Path.Combine(root, "twice.dll"), Path.Combine(root, "twice.pdb"));
Require(second.Succeeded && !second.Changed, "repeated weave was not idempotent");
var corruptPdb = Path.Combine(root, "corrupt.pdb");
File.WriteAllText(corruptPdb, "not a portable pdb");
var corrupt = ShrinkAssemblyWeaver.Weave(sourceAssembly, corruptPdb, references,
Path.Combine(root, "corrupt.dll"), Path.Combine(root, "corrupt.out.pdb"));
Require(!corrupt.Succeeded && !File.Exists(Path.Combine(root, "corrupt.dll")),
"damaged PDB was not rejected atomically");
var missing = ShrinkAssemblyWeaver.Weave(sourceAssembly, null, Array.Empty<string>(),
Path.Combine(root, "missing.dll"), null);
Require(!missing.Succeeded, "missing dependencies were not rejected");
var signedInput = Path.Combine(root, "signed.dll");
using (var assembly = AssemblyDefinition.ReadAssembly(sourceAssembly))
{
assembly.Name.PublicKey = Enumerable.Range(0, 160).Select(value => (byte)(value + 1)).ToArray();
assembly.Name.Attributes |= AssemblyAttributes.PublicKey;
assembly.Write(signedInput);
}
var signed = ShrinkAssemblyWeaver.Weave(signedInput, null, references,
Path.Combine(root, "signed.out.dll"), null);
Require(!signed.Succeeded && signed.Diagnostics.Any(item => item.Message.Contains("Signed assemblies")),
"signed assembly was not rejected");
var parallel = Enumerable.Range(0, 4).Select(index => Task.Run(() =>
{
var output = Path.Combine(root, $"parallel-{index}.dll");
var outputPdb = Path.Combine(root, $"parallel-{index}.pdb");
return ShrinkAssemblyWeaver.Weave(sourceAssembly, sourcePdb, references, output, outputPdb);
})).ToArray();
await Task.WhenAll(parallel);
Require(parallel.All(task => task.Result.Succeeded && task.Result.Changed),
"parallel weaving of independent assemblies failed");
Console.WriteLine("PASS Cecil structure + single/repeat/parallel weave + corrupt PDB/signed/missing dependency guards");
}
finally
{
Directory.Delete(root, true);
}
static void VerifyStructure(string path)
{
using var assembly = AssemblyDefinition.ReadAssembly(path);
var module = assembly.MainModule;
var instance = module.Types.Single(type => type.Name == "InstanceSubscriber");
Require(instance.Interfaces.Any(item => item.InterfaceType.FullName == "ShrinkEventBus.IShrinkGeneratedSubscriber"),
"generated subscriber interface is missing");
Require(instance.Methods.Any(method => method.Name == "ShrinkEventBus.IShrinkGeneratedSubscriber.AttachGenerated"),
"AttachGenerated is missing");
Require(module.Types.Any(type => type.FullName == "ShrinkEventBus.Generated.ShrinkGeneratedStaticBindings"),
"static binding bootstrap is missing");
Require(module.Types.Any(type => type.FullName ==
"ShrinkNetwork.Integration.Generated.ShrinkGeneratedNetworkEventBindings"),
"Network/EventBus generated bootstrap is missing");
var moduleInitializer = module.Types.Single(type => type.Name == "<Module>").Methods.Single(method => method.Name == ".cctor");
Require(moduleInitializer.Body.Instructions.Any(instruction => instruction.Operand is MethodReference method && method.Name == "Register"),
"module initializer does not call static registration");
var attributes = assembly.CustomAttributes.Select(attribute => attribute.AttributeType.FullName).ToArray();
foreach (var expected in new[]
{
"ShrinkSDK.Runtime.ShrinkCodeGenWovenAttribute",
"ShrinkCommand.ShrinkCommandStaticRegistryAttribute",
"ShrinkNetwork.ShrinkNetworkMessageRegistryAttribute",
"ShrinkNetwork.ShrinkNetworkStaticSubscriberRegistryAttribute"
})
Require(attributes.Contains(expected), $"assembly registry is missing: {expected}");
}
static void Require(bool condition, string message)
{
if (!condition) throw new InvalidOperationException(message);
}
static string[] BuildReferences(string sourceDirectory)
{
var referencePackRoot = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".nuget", "packages", "microsoft.netcore.app.ref");
var referencePack = Directory.GetDirectories(referencePackRoot)
.Select(path => Path.Combine(path, "ref", "net8.0"))
.Where(Directory.Exists)
.OrderByDescending(path => path, StringComparer.OrdinalIgnoreCase)
.First();
return Directory.GetFiles(sourceDirectory, "*.dll")
.Concat(Directory.GetFiles(referencePack, "*.dll"))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
}
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<AssemblyName>ShrinkSDK.InstallerFixture</AssemblyName>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkInstaller\Godot~\ShrinkSDK.Godot.Installer.csproj" />
</ItemGroup>
</Project>
+65
View File
@@ -0,0 +1,65 @@
#nullable enable
using System;
using System.IO;
using System.Linq;
using System.Xml.Linq;
using ShrinkSDK.Godot.Editor;
using ShrinkSDK.Installer;
var root = Path.Combine(Path.GetTempPath(), "ShrinkSDK.InstallerFixture", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
try
{
var project = Path.Combine(root, "Fixture.csproj");
File.WriteAllText(project, """
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup><TargetFramework>net8.0</TargetFramework><FixtureValue>keep</FixtureValue></PropertyGroup>
<ItemGroup>
<PackageReference Include="Existing.Package" Version="1.2.3" />
<PackageReference Include="Child.Version.Package"><Version>3.4.5</Version></PackageReference>
</ItemGroup>
</Project>
""");
var config = Path.Combine(root, "NuGet.Config");
File.WriteAllText(config, """
<configuration><packageSources><add key="Existing" value="https://example.invalid/v3/index.json" /></packageSources></configuration>
""");
ShrinkProjectPackageEditor.SetPackageReference(project, "ShrinkSDK.EventBus", "2.1.0");
ShrinkProjectPackageEditor.SetPackageReference(project, "ShrinkSDK.EventBus", "2.1.0");
var installed = XDocument.Load(project);
Require(installed.Descendants("PackageReference").Count(element =>
(string?)element.Attribute("Include") == "ShrinkSDK.EventBus") == 1, "package reference was duplicated");
Require(installed.ToString().Contains("Existing.Package") && installed.ToString().Contains("FixtureValue"),
"unrelated project content was changed");
var installedVersions = ShrinkProjectPackageEditor.ReadPackageReferences(project);
Require(installedVersions["ShrinkSDK.EventBus"] == "2.1.0", "installed version was not read correctly");
Require(installedVersions["Child.Version.Package"] == "3.4.5", "child Version element was not read correctly");
Require(ShrinkPackageVersion.Classify("2.2.0", "2.1.0") == ShrinkPackageVersionRelation.Newer,
"newer installed version was classified as outdated");
Require(ShrinkPackageVersion.Classify("2.1.0-preview.10", "2.1.0-preview.2") ==
ShrinkPackageVersionRelation.Newer, "prerelease versions were not ordered semantically");
ShrinkProjectPackageEditor.SetPackageReference(project, "ShrinkSDK.EventBus", null);
Require(!XDocument.Load(project).Descendants("PackageReference").Any(element =>
(string?)element.Attribute("Include") == "ShrinkSDK.EventBus"), "package reference was not removed");
ShrinkProjectPackageEditor.EnsureShrinkFeed(config);
ShrinkProjectPackageEditor.EnsureShrinkFeed(config);
var sources = XDocument.Load(config).Descendants("add").ToArray();
Require(sources.Count(element => (string?)element.Attribute("key") == "ShrinkSDK") == 1,
"ShrinkSDK feed was duplicated");
Require(sources.Any(element => (string?)element.Attribute("key") == "Existing"),
"existing NuGet source was removed");
Console.WriteLine("PASS Godot Installer atomic package/feed install-update-remove merge");
}
finally
{
Directory.Delete(root, true);
}
static void Require(bool condition, string message)
{
if (!condition) throw new InvalidOperationException(message);
}
@@ -0,0 +1,7 @@
using ShrinkNetwork;
[ShrinkNetworkMessage(49999, "duplicate")]
public sealed class DuplicateMessageA : IShrinkNetworkMessage { }
[ShrinkNetworkMessage(49999, "duplicate")]
public sealed class DuplicateMessageB : IShrinkNetworkMessage { }
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AssemblyName>ShrinkSDK.InvalidCodeGenFixture</AssemblyName>
<ShrinkCodeGenEnabled>true</ShrinkCodeGenEnabled>
<RunAnalyzers>false</RunAnalyzers>
<_ShrinkCodeGenTaskAssembly>$(MSBuildThisFileDirectory)..\..\..\Assets\Modules\ShrinkShared.CodeGen\DotNet~\ShrinkSDK.CodeGen.Task\bin\$(Configuration)\net8.0\ShrinkSDK.CodeGen.Task.dll</_ShrinkCodeGenTaskAssembly>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkRuntime.Abstractions\DotNet~\ShrinkSDK.Runtime.Abstractions.csproj" />
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkNetwork\DotNet~\ShrinkSDK.Network.csproj" />
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkShared.CodeGen\DotNet~\ShrinkSDK.CodeGen.Task\ShrinkSDK.CodeGen.Task.csproj" ReferenceOutputAssembly="false" />
</ItemGroup>
<Import Project="..\..\..\Assets\Modules\ShrinkShared.CodeGen\DotNet~\ShrinkSDK.CodeGen.Task\buildTransitive\ShrinkSDK.CodeGen.targets" />
</Project>
+27
View File
@@ -0,0 +1,27 @@
using ShrinkCommand;
using ShrinkEventBus;
using ShrinkModFramework;
[ShrinkMod("fixture.mod", "Fixture Mod", "1.0.0")]
public sealed class FixtureMod : ShrinkModBase, IShrinkModUnload
{
public bool Ready { get; private set; }
public int UnloadCalls { get; private set; }
public override void OnReady(ShrinkModContext context) => Ready = true;
public void OnUnload(ShrinkModContext context) => UnloadCalls++;
}
public readonly struct ModFixtureEvent : IShrinkEvent { }
[ShrinkEventSubscriber]
public static class ModFixtureEvents
{
public static int Calls { get; private set; }
[ShrinkSubscribe] private static void Handle(ModFixtureEvent value) => Calls++;
}
[ShrinkCommandSubscriber]
public static class ModFixtureCommands
{
[ShrinkCommand("mod-fixture/ping")] public static void Ping() { }
}
+15
View File
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<AssemblyName>ShrinkSDK.ModFixture</AssemblyName>
<ShrinkCodeGenEnabled>true</ShrinkCodeGenEnabled>
<_ShrinkCodeGenTaskAssembly>$(MSBuildThisFileDirectory)..\..\..\Assets\Modules\ShrinkShared.CodeGen\DotNet~\ShrinkSDK.CodeGen.Task\bin\$(Configuration)\net8.0\ShrinkSDK.CodeGen.Task.dll</_ShrinkCodeGenTaskAssembly>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkRuntime.Abstractions\DotNet~\ShrinkSDK.Runtime.Abstractions.csproj" />
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkModFramework\DotNet~\ShrinkSDK.ModFramework.csproj" />
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkEventBus\DotNet~\ShrinkSDK.EventBus.csproj" />
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkCommand\DotNet~\ShrinkSDK.Command.csproj" />
</ItemGroup>
<Import Project="..\..\..\Assets\Modules\ShrinkShared.CodeGen\DotNet~\ShrinkSDK.CodeGen.Task\buildTransitive\ShrinkSDK.CodeGen.targets" />
</Project>
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<AssemblyName>ShrinkSDK.ModHostFixture</AssemblyName>
<ShrinkCodeGenEnabled>false</ShrinkCodeGenEnabled>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkModFramework\Godot~\ShrinkSDK.ModFramework.Godot.csproj" />
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkEventBus\DotNet~\ShrinkSDK.EventBus.csproj" />
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkCommand\DotNet~\ShrinkSDK.Command.csproj" />
</ItemGroup>
</Project>
+37
View File
@@ -0,0 +1,37 @@
#nullable enable
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using ShrinkCommand;
using ShrinkEventBus;
using ShrinkModFramework.Godot;
if (args.Length != 1 || !File.Exists(args[0]))
throw new ArgumentException("Pass the woven mod assembly path.");
using var loader = new ShrinkGodotModLoader();
var instances = loader.Load(args[0]);
if (instances.Count != 1) throw new InvalidOperationException("Mod entry was not discovered.");
var mod = instances[0];
var type = mod.GetType();
if (type.GetProperty("Ready")?.GetValue(mod) is not true)
throw new InvalidOperationException("Mod lifecycle did not reach OnReady.");
var assembly = type.Assembly;
if (assembly.GetCustomAttributes(typeof(ShrinkCommandStaticRegistryAttribute), false).Length != 1)
throw new InvalidOperationException("Mod command registry metadata is missing.");
var eventType = assembly.GetType("ModFixtureEvent") ?? throw new InvalidOperationException("Mod event is missing.");
var post = typeof(EventBus).GetMethods(BindingFlags.Public | BindingFlags.Static)
.Single(method => method.Name == "Post" && method.IsGenericMethodDefinition && method.GetParameters().Length == 1)
.MakeGenericMethod(eventType);
post.Invoke(null, new[] { Activator.CreateInstance(eventType) });
var eventSubscriber = assembly.GetType("ModFixtureEvents") ?? throw new InvalidOperationException("Mod static subscriber is missing.");
if ((int?)eventSubscriber.GetProperty("Calls", BindingFlags.Public | BindingFlags.Static)?.GetValue(null) != 1)
throw new InvalidOperationException("Mod static EventBus binding did not run.");
if (!loader.Unload(args[0])) throw new InvalidOperationException("Mod unload failed.");
if ((int?)type.GetProperty("UnloadCalls")?.GetValue(mod) != 1)
throw new InvalidOperationException("Mod OnUnload was not called.");
if (loader.LoadedAssemblyPaths.Count != 0)
throw new InvalidOperationException("Mod remained registered after unload.");
Console.WriteLine("PASS external netstandard2.1 mod CodeGen + Godot AssemblyLoadContext lifecycle");
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<AssemblyName>ShrinkSDK.NuGetConsumer</AssemblyName>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ShrinkSDK.EventBus" Version="2.1.0" />
<PackageReference Include="ShrinkSDK.Command" Version="0.3.0" />
<PackageReference Include="ShrinkSDK.Network" Version="0.3.0" />
</ItemGroup>
</Project>
+46
View File
@@ -0,0 +1,46 @@
#nullable enable
using System;
using System.Linq;
using ShrinkCommand;
using ShrinkEventBus;
using ShrinkNetwork;
using ShrinkSDK.Runtime;
var assembly = typeof(Program).Assembly;
var marker = assembly.GetCustomAttributes(typeof(ShrinkCodeGenWovenAttribute), false)
.Cast<ShrinkCodeGenWovenAttribute>().SingleOrDefault()
?? throw new InvalidOperationException("buildTransitive CodeGen did not run.");
using var binding = EventBus.Attach(new NuGetSubscriber());
EventBus.Post(new NuGetEvent());
if (NuGetSubscriber.Calls != 1 || NuGetStaticSubscriber.Calls != 1)
throw new InvalidOperationException("NuGet EventBus weaving failed.");
if (assembly.GetCustomAttributes(typeof(ShrinkCommandStaticRegistryAttribute), false).Length != 1 ||
assembly.GetCustomAttributes(typeof(ShrinkNetworkMessageRegistryAttribute), false).Length != 1)
throw new InvalidOperationException("NuGet registries are missing.");
Console.WriteLine($"PASS NuGet buildTransitive CodeGen {marker.WeaverVersion}");
public readonly struct NuGetEvent : IShrinkEvent { }
[ShrinkEventSubscriber]
public sealed class NuGetSubscriber
{
public static int Calls;
[ShrinkSubscribe] private void Handle(NuGetEvent value) => Calls++;
}
[ShrinkEventSubscriber]
public static class NuGetStaticSubscriber
{
public static int Calls;
[ShrinkSubscribe] private static void Handle(NuGetEvent value) => Calls++;
}
[ShrinkCommandSubscriber]
public static class NuGetCommands
{
[ShrinkCommand("nuget/smoke")] public static void Smoke() { }
}
[ShrinkNetworkMessage(43001, "nuget/smoke")]
public sealed class NuGetMessage : IShrinkNetworkMessage { }
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<LangVersion>9.0</LangVersion>
<Nullable>enable</Nullable>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
<AssemblyName>ShrinkSDK.UnityAdapterCompile</AssemblyName>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\..\..\Assets\Modules\ShrinkShared.CodeGen\Editor\Core\*.cs" />
<Compile Include="..\..\..\Assets\Modules\ShrinkShared.CodeGen\Editor\UnityShrinkCodeGenAdapter.cs" />
<Compile Include="..\..\..\Assets\Modules\ShrinkShared.CodeGen\Editor\ShrinkRegistryILPostProcessor.cs" />
<Compile Include="..\..\..\Assets\Modules\ShrinkEventBus\CodeGen\Editor\EventBusILPostProcessor.cs" />
<Reference Include="Unity.CompilationPipeline.Common">
<HintPath>D:\UnityEditor\2022.3.62f3\Editor\Data\Managed\Unity.CompilationPipeline.Common.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Mono.Cecil">
<HintPath>..\..\..\Library\PackageCache\com.unity.nuget.mono-cecil@1.11.4\Mono.Cecil.dll</HintPath>
<Private>false</Private>
</Reference>
</ItemGroup>
</Project>
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"dependencies": { "dependencies": {
"com.cysharp.unitask": "https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask", "com.cysharp.unitask": "https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask#7c0f199fe0d3fc528024488ccd671e6c7b27745b",
"com.gamebooom.unity.mcp": "https://github.com/FunplayAI/funplay-unity-mcp.git", "com.gamebooom.unity.mcp": "https://github.com/FunplayAI/funplay-unity-mcp.git#20e2e09e0cfd6e0b807a2163035f07db67a48018",
"com.unity.collab-proxy": "2.12.4", "com.unity.collab-proxy": "2.12.4",
"com.unity.entities": "1.0.11", "com.unity.entities": "1.0.11",
"com.unity.feature.2d": "2.0.1", "com.unity.feature.2d": "2.0.1",
+3 -3
View File
@@ -1,14 +1,14 @@
{ {
"dependencies": { "dependencies": {
"com.cysharp.unitask": { "com.cysharp.unitask": {
"version": "https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask", "version": "https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask#7c0f199fe0d3fc528024488ccd671e6c7b27745b",
"depth": 0, "depth": 0,
"source": "git", "source": "git",
"dependencies": {}, "dependencies": {},
"hash": "a9e27c03d411d2fca01cc7410c24c97cd77cb539" "hash": "7c0f199fe0d3fc528024488ccd671e6c7b27745b"
}, },
"com.gamebooom.unity.mcp": { "com.gamebooom.unity.mcp": {
"version": "https://github.com/FunplayAI/funplay-unity-mcp.git", "version": "https://github.com/FunplayAI/funplay-unity-mcp.git#20e2e09e0cfd6e0b807a2163035f07db67a48018",
"depth": 0, "depth": 0,
"source": "git", "source": "git",
"dependencies": { "dependencies": {
+87 -10
View File
@@ -1,6 +1,8 @@
# ShrinkSDK Workspace # ShrinkSDK Workspace
ShrinkSDK 的 Workspace 用于跨包集成、场景验证、生成器验证和 SDK 文档。每个 `Assets/Modules/<Module>` 是独立公开仓库的 Git submodule;根仓库保留相邻 `.meta`,因此不要删除或重新生成这些文件 ShrinkSDK 是一组可独立组合的 Unity / Godot C# 运行时模块。共享内核以 `netstandard2.1` 为最低目标;Unity 侧支持 Unity 2022.3Godot 侧首期支持 Godot 4.6.3 Mono 和桌面平台
Workspace 用于跨包集成、Unity 场景验证、Godot 消费者验证和 CodeGen 织入测试。每个 `Assets/Modules/<Module>` 是独立公开仓库的 Git submodule;根仓库保留相邻 `.meta`,不要删除或重新生成这些文件。
## 获取 Workspace ## 获取 Workspace
@@ -10,11 +12,23 @@ cd Workspace
git submodule update --init --recursive git submodule update --init --recursive
``` ```
所有模块都可以独立打开其 `Development~/UnityProject`。根 Workspace 用于跨包组合与验收。 所有 Unity 模块都可以独立打开其 `Development~/UnityProject`。根 Workspace 用于跨包组合与验收Godot 综合示例位于 `Godot/Samples/ShrinkSDK.Godot.Sample`
## 安装 SDK ## Unity 安装与使用
首选公开 Gitea NPM/UPM registry,在消费者项目的 `Packages/manifest.json` 中保留既有配置并加入: ### 通过 Installer 安装
在 Unity Package Manager 中选择 **Add package from git URL**,添加:
```text
https://git.crash.work/ShrinkSDK/Installer.git#main
```
正式项目应在对应版本发布后把 `main` 换成固定 tag。安装完成后打开 `ShrinkSDK/包管理`,选择单个模块或推荐组合。Installer 会合并 ShrinkSDK scoped registry、安装精确版本依赖,并分别显示未安装、可升级、已安装和已安装较新版本;不会把较新版本降级到目录版本。
### 手工安装
也可以在消费者项目的 `Packages/manifest.json` 中保留原有内容并加入 registry:
```json ```json
{ {
@@ -24,22 +38,85 @@ git submodule update --init --recursive
"url": "https://git.crash.work/api/packages/ShrinkSDK/npm/", "url": "https://git.crash.work/api/packages/ShrinkSDK/npm/",
"scopes": ["com.cneicy"] "scopes": ["com.cneicy"]
} }
] ],
"dependencies": {
"com.cneicy.shrink-app-starter-basic": "0.3.0"
}
} }
``` ```
然后按精确版本添加包,例如 `com.cneicy.shrink-app-starter-basic: 0.1.0`。Git 备用安装同样必须固定标签,例如: 如果项目已经有 `scopedRegistries``dependencies`,只合并对应条目,不要覆盖原有配置。Git 安装同样建议固定 tag,例如:
```text ```text
https://git.crash.work/ShrinkSDK/ShrinkEventBus.git#v2.0.0 https://git.crash.work/ShrinkSDK/ShrinkEventBus.git#v2.1.0
``` ```
也可先通过固定标签安装 Editor 引导包: ### 快速开始
安装 `ShrinkApp.Starter.Basic` 后,执行 `ShrinkSDK/应用/基础起步/创建默认 Context 组合`。菜单会生成默认配置和入口场景;打开 `Assets/Scenes/ShrinkAppEntry.unity` 即可运行 Command、DataSaver、Network 和 App 的基础组合。
EventBus、Command、Network 和 App 的特性注册由 Unity ILPostProcessor 在编译期织入,不需要运行时反射扫描或手工维护注册表。修改带 ShrinkSDK 特性的代码后,等待 Unity 完成重新编译,再进入 Play Mode。
## Godot 4.6 C# 安装与使用
Godot 项目需要 Godot 4.6.3 Mono 和 .NET 8 SDK。使用现有 Workspace 验证时,直接用 Godot 打开:
```text ```text
https://git.crash.work/ShrinkSDK/Installer.git#v0.1.2 Godot/Samples/ShrinkSDK.Godot.Sample/project.godot
``` ```
随后在 Unity 打开 `ShrinkSDK/Packages`。安装器只合并 `com.cneicy` registry 并安装内置目录中的固定版本,不写认证信息、不复制模板到 `Assets` 外部 Godot C# 项目通过 NuGet 使用 ShrinkSDK。先在项目级 `NuGet.Config` 中加入软件源:
```xml
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="ShrinkSDK" value="https://git.crash.work/api/packages/ShrinkSDK/nuget/index.json" />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
</packageSources>
</configuration>
```
然后为 Godot 的 `.csproj` 安装宿主和需要的模块,例如:
```powershell
dotnet add package ShrinkSDK.Godot --version 0.1.0
dotnet add package ShrinkSDK.App.Starter.Basic --version 0.3.0
dotnet add package ShrinkSDK.EventBus --version 2.1.0
dotnet restore
dotnet build
```
需要自动注册的包会传递引入 `ShrinkSDK.CodeGen`。CodeGen 默认在 `CoreCompile` 后织入当前项目程序集;构建日志应出现 `[ShrinkSDK.CodeGen]`,无需额外添加 Source Generator、反射扫描或手工注册表。
主场景根节点可继承 `ShrinkGodotHost`
```csharp
using Cysharp.Threading.Tasks;
using ShrinkSDK.Godot;
public partial class GameRoot : ShrinkGodotHost
{
public override void _Ready()
{
base._Ready();
StartAsync().Forget();
}
private async UniTaskVoid StartAsync()
{
RequireWoven(GetType().Assembly);
await StartAppAsync();
}
}
```
`ShrinkGodotHost``_Ready` 中安装平台服务,在 `_Process` 中泵送主线程和网络队列,并转发暂停、恢复与退出生命周期。Godot Installer 插件的安装、包管理面板、CodeGen 状态和命令行验证方式见 [Godot/README.md](Godot/README.md)。
## 开发与发布
发布、子模块迁移和独立开发宿主生成规则位于 [Tools/RepositoryMigration/Initialize-ShrinkSdkPackageRepositories.ps1](Tools/RepositoryMigration/Initialize-ShrinkSdkPackageRepositories.ps1)。架构约束与跨包验证要求见 [DESIGN.md](DESIGN.md)。 发布、子模块迁移和独立开发宿主生成规则位于 [Tools/RepositoryMigration/Initialize-ShrinkSdkPackageRepositories.ps1](Tools/RepositoryMigration/Initialize-ShrinkSdkPackageRepositories.ps1)。架构约束与跨包验证要求见 [DESIGN.md](DESIGN.md)。
## 第三方与合规
概念参考(NeoForge / Minecraft Forge、MessagePipe)与随仓库分发的第三方二进制(Kcp-CSharp.dll 等)的许可证遵从情况见 [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md)。
+89
View File
@@ -0,0 +1,89 @@
# 第三方声明(THIRD-PARTY NOTICES
本文件说明 ShrinkSDK Workspace 的第三方概念参考、依赖与对应许可证遵从情况。最后核对日期:2026-08-29。
## 结论速览
| 对象 | 关系 | 许可证 | 义务 |
| --- | --- | --- | --- |
| NeoForge / Minecraft Forge | 设计概念参考(未复制代码) | LGPL-2.1 | 无强制义务,本文档致谢 |
| MessagePipeCysharp | 设计概念参考(未复制代码、未引入包) | MIT | 无强制义务,本文档致谢 |
| AlicizaXGitHub 组织) | 无引用 | MIT / Apache-2.0 | 无 |
| Kcp-CSharp.dll | 内嵌二进制(`Assets/Modules/ShrinkNetwork/Plugins/`) | MIT | 保留版权与许可声明(见下) |
| System.Runtime.CompilerServices.Unsafe.dll | 内嵌二进制(`Assets/Modules/ShrinkNetwork/Plugins/`) | MIT | 保留版权与许可声明(见下) |
| UniTask | 包管理器依赖(不随本仓库再分发) | MIT | 许可文本随包分发 |
| MessagePack | 包管理器依赖(不随本仓库再分发) | MIT | 许可文本随包分发 |
| Newtonsoft.Json | 包管理器依赖(不随本仓库再分发) | MIT | 许可文本随包分发 |
## 概念参考归致
### NeoForge / Minecraft ForgeLGPL-2.1
`ShrinkEventBus``ShrinkModFramework` 在 API 语义与设计思路上参考了 Forge / NeoForge
- 五级订阅优先级 `ShrinkEventPriority.Highest / High / Normal / Low / Lowest` 对应 Forge `EventPriority`
- `[ShrinkSubscribe(receiveCanceled: ...)]` 对应 `@SubscribeEvent(receiveCanceled = ...)`
- `EventResult { DEFAULT, ALLOW, DENY }` 对应 Forge `Event.Result`
- `IShrinkEventBus.Attach(object)` 的对象注册范式对应 `MinecraftForge.EVENT_BUS.register(...)`
以上为 API 概念与命名语义层面的借鉴。实现(IL 后处理静态绑定、泛型静态槽位通道、UniTask 调度集成、写时复制 handler 快照等)均为本项目原创,未复制 NeoForge / Minecraft Forge 的任何源代码,因此不构成 LGPL-2.1 意义上的衍生作品。该边界与 `Docs/JustAnyProjectArchitectureAudit.md`A.3 参考边界)的既有声明一致。
### MessagePipeMIT
MessagePipe 未作为依赖引入,EventBus 也不提供 `IPublisher/ISubscriber` 拆分或 DI 集成,与 MessagePipe 的 API 形态不构成高度相似。仅借鉴其"独立 broker + 稳定 handler 快照 + 低分配发布路径"的实现思路(见 `Docs/JustAnyProjectArchitectureAudit.md` A.3),不构成代码衍生。
### AlicizaX
本仓库及全部子模块未引用、未复制、未派生 https://github.com/AlicizaX 下任何仓库的代码或资产。
## 随本仓库分发的第三方二进制
以下 DLL 直接存放在 `Assets/Modules/ShrinkNetwork/Plugins/`,随仓库与构建产物分发,按 MIT 要求保留其版权与许可声明:
### Kcp-CSharp.dll
- 来源:https://github.com/Molth/Kcp-CSharp NuGet 包 `Kcp-CSharp` 1.0.8
- 用途:`ShrinkNetwork` KCP 传输层
- 许可证:MITCopyright (c) 2024 Nevin
- 上游协议实现 KCPhttps://github.com/skywind3000/kcp MITCopyright (c) 2017 Lin Wei (skywind3000 at gmail.com)
### System.Runtime.CompilerServices.Unsafe.dll
- 来源:NuGet 包 `System.Runtime.CompilerServices.Unsafe`
- 许可证:MITCopyright (c) .NET Foundation and Contributors
### MIT 许可证全文(适用于上述两个组件)
```text
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
## 包管理器依赖(不随本仓库再分发)
以下依赖通过 Unity Package Manager 或 NuGet 引入,许可证文本随各自包分发,此处仅作登记:
- UniTask 2.5.10 — MIT — Copyright (c) 2019 Yoshifumi Kawai / Cysharp, Inc. — https://github.com/Cysharp/UniTask
- MessagePack 3.1.4 — MIT — Copyright (c) 2017 Yoshifumi Kawai and contributors — https://github.com/MessagePack-CSharp/MessagePack-CSharp
- Newtonsoft.Json 13.0.3 — MIT — Copyright (c) 2007 James Newton-King — https://github.com/JamesNK/Newtonsoft.Json
- Unity 官方包 — 按 Unity 各自许可条款分发
## 维护者注意
- `ShrinkNetwork` 模块独立发布时,应把"随本仓库分发的第三方二进制"一节同步进该模块仓库(模块是独立公开仓库,根仓库声明不能覆盖其独立分发路径)。
- Mod SDK 导出器(`ShrinkModSdkExporter`)目前只导出 ShrinkSDK 自有程序集;若未来把上述第三方 DLL 一并打包进导出包,必须在导出包内附带对应许可声明。
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
set -euo pipefail
workspace_root="${1:-$PWD}"
workspace_root="$(cd "$workspace_root" && pwd)"
cd "$workspace_root"
if [[ ! -f .gitmodules ]]; then
echo "Workspace .gitmodules was not found: $workspace_root" >&2
exit 1
fi
mapfile -t declared_paths < <(
git config --file .gitmodules --get-regexp '^submodule\..*\.path$' |
awk '{ print $2 }'
)
if [[ "${#declared_paths[@]}" -eq 0 ]]; then
echo "No submodules are declared in .gitmodules." >&2
exit 1
fi
mapfile -t top_level_status < <(git submodule status)
if [[ "${#top_level_status[@]}" -ne "${#declared_paths[@]}" ]]; then
echo "Submodule count mismatch: declared=${#declared_paths[@]} status=${#top_level_status[@]}" >&2
exit 1
fi
for relative_path in "${declared_paths[@]}"; do
if [[ ! -e "$relative_path/.git" ]]; then
echo "Declared submodule is not initialized: $relative_path" >&2
exit 1
fi
done
recursive_status="$(git submodule status --recursive)"
if grep -Eq '^[+-U]' <<<"$recursive_status"; then
echo "One or more submodules are missing, conflicted, or not at the recorded revision:" >&2
grep -E '^[+-U]' <<<"$recursive_status" >&2
exit 1
fi
printf '%s\n' "$recursive_status"
echo "Submodule validation passed: declared=${#declared_paths[@]}"
@@ -0,0 +1,44 @@
[CmdletBinding(DefaultParameterSetName = 'Check')]
param(
[Parameter(Mandatory, ParameterSetName = 'Check')]
[switch]$Check,
[Parameter(Mandatory, ParameterSetName = 'Bump')]
[Parameter(Mandatory, ParameterSetName = 'Version')]
[string]$Package,
[Parameter(Mandatory, ParameterSetName = 'Bump')]
[ValidateSet('major', 'minor', 'patch')]
[string]$Bump,
[Parameter(Mandatory, ParameterSetName = 'Version')]
[ValidatePattern('^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$')]
[string]$Version,
[Parameter(ParameterSetName = 'Bump')]
[Parameter(ParameterSetName = 'Version')]
[switch]$DryRun
)
$ErrorActionPreference = 'Stop'
$scriptPath = Join-Path $PSScriptRoot 'update-shrinksdk-versions.mjs'
$arguments = @($scriptPath)
if ($Check) {
$arguments += '--check'
} else {
$arguments += @('--package', $Package)
if ($PSCmdlet.ParameterSetName -eq 'Bump') {
$arguments += @('--bump', $Bump)
} else {
$arguments += @('--version', $Version)
}
if ($DryRun) {
$arguments += '--dry-run'
}
}
& node @arguments
if ($LASTEXITCODE -ne 0) {
throw "ShrinkSDK version tool failed with exit code $LASTEXITCODE."
}
+302
View File
@@ -0,0 +1,302 @@
{
"schemaVersion": 1,
"unityPackages": [
{
"directory": "ShrinkApp.Core",
"name": "com.cneicy.shrink-app-core",
"version": "0.2.0",
"repository": "https://git.crash.work/ShrinkSDK/ShrinkApp.Core.git",
"nugetPackages": [
{
"id": "ShrinkSDK.App.Core",
"version": "0.2.0",
"project": "Assets/Modules/ShrinkApp.Core/DotNet~/ShrinkSDK.App.Core.csproj"
}
]
},
{
"directory": "ShrinkApp.Starter.Basic",
"name": "com.cneicy.shrink-app-starter-basic",
"version": "0.3.0",
"repository": "https://git.crash.work/ShrinkSDK/ShrinkApp.Starter.Basic.git",
"nugetPackages": [
{
"id": "ShrinkSDK.App.Starter.Basic",
"version": "0.3.0",
"project": "Assets/Modules/ShrinkApp.Starter.Basic/DotNet~/ShrinkSDK.App.Starter.Basic.csproj"
}
]
},
{
"directory": "ShrinkCommand",
"name": "com.cneicy.shrink-command",
"version": "0.3.0",
"repository": "https://git.crash.work/ShrinkSDK/ShrinkCommand.git",
"nugetPackages": [
{
"id": "ShrinkSDK.Command",
"version": "0.3.0",
"project": "Assets/Modules/ShrinkCommand/DotNet~/ShrinkSDK.Command.csproj"
}
]
},
{
"directory": "ShrinkCommand.Integration.App",
"name": "com.cneicy.shrink-command-integration-app",
"version": "0.2.0",
"repository": "https://git.crash.work/ShrinkSDK/ShrinkCommand.Integration.App.git",
"nugetPackages": [
{
"id": "ShrinkSDK.Command.Integration.App",
"version": "0.2.0",
"project": "Assets/Modules/ShrinkCommand.Integration.App/DotNet~/ShrinkSDK.Command.Integration.App.csproj"
}
]
},
{
"directory": "ShrinkCommand.Integration.EventBus",
"name": "com.cneicy.shrink-command-integration-eventbus",
"version": "0.2.0",
"repository": "https://git.crash.work/ShrinkSDK/ShrinkCommand.Integration.EventBus.git",
"nugetPackages": [
{
"id": "ShrinkSDK.Command.Integration.EventBus",
"version": "0.2.0",
"project": "Assets/Modules/ShrinkCommand.Integration.EventBus/DotNet~/ShrinkSDK.Command.Integration.EventBus.csproj"
}
]
},
{
"directory": "ShrinkCommand.Integration.Network",
"name": "com.cneicy.shrink-command-integration-network",
"version": "0.2.0",
"repository": "https://git.crash.work/ShrinkSDK/ShrinkCommand.Integration.Network.git",
"nugetPackages": [
{
"id": "ShrinkSDK.Command.Integration.Network",
"version": "0.2.0",
"project": "Assets/Modules/ShrinkCommand.Integration.Network/DotNet~/ShrinkSDK.Command.Integration.Network.csproj"
}
]
},
{
"directory": "ShrinkContext.AppAdapter",
"name": "com.cneicy.shrink-context-app-adapter",
"version": "0.2.0",
"repository": "https://git.crash.work/ShrinkSDK/ShrinkContext.AppAdapter.git",
"nugetPackages": [
{
"id": "ShrinkSDK.Context.AppAdapter",
"version": "0.2.0",
"project": "Assets/Modules/ShrinkContext.AppAdapter/DotNet~/ShrinkSDK.Context.AppAdapter.csproj"
}
]
},
{
"directory": "ShrinkContext.Core",
"name": "com.cneicy.shrink-context-core",
"version": "0.2.0",
"repository": "https://git.crash.work/ShrinkSDK/ShrinkContext.Core.git",
"nugetPackages": [
{
"id": "ShrinkSDK.Context.Core",
"version": "0.2.0",
"project": "Assets/Modules/ShrinkContext.Core/DotNet~/ShrinkSDK.Context.Core.csproj"
}
]
},
{
"directory": "ShrinkContext.EventBusAdapter",
"name": "com.cneicy.shrink-context-eventbus-adapter",
"version": "0.2.0",
"repository": "https://git.crash.work/ShrinkSDK/ShrinkContext.EventBusAdapter.git",
"nugetPackages": [
{
"id": "ShrinkSDK.Context.EventBusAdapter",
"version": "0.2.0",
"project": "Assets/Modules/ShrinkContext.EventBusAdapter/DotNet~/ShrinkSDK.Context.EventBusAdapter.csproj"
}
]
},
{
"directory": "ShrinkDataSaver",
"name": "com.cneicy.shrink-datasaver",
"version": "2.3.0",
"repository": "https://git.crash.work/ShrinkSDK/ShrinkDataSaver.git",
"nugetPackages": [
{
"id": "ShrinkSDK.DataSaver",
"version": "2.3.0",
"project": "Assets/Modules/ShrinkDataSaver/DotNet~/ShrinkSDK.DataSaver.csproj"
}
]
},
{
"directory": "ShrinkDataSaver.Integration.App",
"name": "com.cneicy.shrink-datasaver-integration-app",
"version": "0.2.0",
"repository": "https://git.crash.work/ShrinkSDK/ShrinkDataSaver.Integration.App.git",
"nugetPackages": [
{
"id": "ShrinkSDK.DataSaver.Integration.App",
"version": "0.2.0",
"project": "Assets/Modules/ShrinkDataSaver.Integration.App/DotNet~/ShrinkSDK.DataSaver.Integration.App.csproj"
}
]
},
{
"directory": "ShrinkDataSaver.Integration.EventBus",
"name": "com.cneicy.shrink-datasaver-integration-eventbus",
"version": "2.2.0",
"repository": "https://git.crash.work/ShrinkSDK/ShrinkDataSaver.Integration.EventBus.git",
"nugetPackages": [
{
"id": "ShrinkSDK.DataSaver.Integration.EventBus",
"version": "2.2.0",
"project": "Assets/Modules/ShrinkDataSaver.Integration.EventBus/DotNet~/ShrinkSDK.DataSaver.Integration.EventBus.csproj"
}
]
},
{
"directory": "ShrinkEventBus",
"name": "com.cneicy.shrink-eventbus",
"version": "2.1.0",
"repository": "https://git.crash.work/ShrinkSDK/ShrinkEventBus.git",
"nugetPackages": [
{
"id": "ShrinkSDK.EventBus",
"version": "2.1.0",
"project": "Assets/Modules/ShrinkEventBus/DotNet~/ShrinkSDK.EventBus.csproj"
}
]
},
{
"directory": "ShrinkEventBus.Entities",
"name": "com.cneicy.shrink-eventbus-entities",
"version": "0.1.2",
"repository": "https://git.crash.work/ShrinkSDK/ShrinkEventBus.Entities.git"
},
{
"directory": "ShrinkInstaller",
"name": "com.cneicy.shrink-installer",
"version": "0.2.6",
"repository": "https://git.crash.work/ShrinkSDK/Installer.git",
"nugetPackages": [
{
"id": "ShrinkSDK.Godot.Installer",
"version": "0.1.3",
"project": "Assets/Modules/ShrinkInstaller/Godot~/ShrinkSDK.Godot.Installer.csproj"
}
]
},
{
"directory": "ShrinkModFramework",
"name": "com.cneicy.shrink-mod-framework",
"version": "0.3.0",
"repository": "https://git.crash.work/ShrinkSDK/ShrinkModFramework.git",
"nugetPackages": [
{
"id": "ShrinkSDK.ModFramework",
"version": "0.3.0",
"project": "Assets/Modules/ShrinkModFramework/DotNet~/ShrinkSDK.ModFramework.csproj"
},
{
"id": "ShrinkSDK.ModFramework.Godot",
"version": "0.1.0",
"project": "Assets/Modules/ShrinkModFramework/Godot~/ShrinkSDK.ModFramework.Godot.csproj"
}
]
},
{
"directory": "ShrinkNetwork",
"name": "com.cneicy.shrink-network",
"version": "0.3.0",
"repository": "https://git.crash.work/ShrinkSDK/ShrinkNetwork.git",
"nugetPackages": [
{
"id": "ShrinkSDK.Network",
"version": "0.3.0",
"project": "Assets/Modules/ShrinkNetwork/DotNet~/ShrinkSDK.Network.csproj"
}
]
},
{
"directory": "ShrinkNetwork.Integration.App",
"name": "com.cneicy.shrink-network-integration-app",
"version": "0.2.0",
"repository": "https://git.crash.work/ShrinkSDK/ShrinkNetwork.Integration.App.git",
"nugetPackages": [
{
"id": "ShrinkSDK.Network.Integration.App",
"version": "0.2.0",
"project": "Assets/Modules/ShrinkNetwork.Integration.App/DotNet~/ShrinkSDK.Network.Integration.App.csproj"
}
]
},
{
"directory": "ShrinkNetwork.Integration.EventBus",
"name": "com.cneicy.shrink-network-integration-eventbus",
"version": "0.2.0",
"repository": "https://git.crash.work/ShrinkSDK/ShrinkNetwork.Integration.EventBus.git",
"nugetPackages": [
{
"id": "ShrinkSDK.Network.Integration.EventBus",
"version": "0.2.0",
"project": "Assets/Modules/ShrinkNetwork.Integration.EventBus/DotNet~/ShrinkSDK.Network.Integration.EventBus.csproj"
}
]
},
{
"directory": "ShrinkRuntime.Abstractions",
"name": "com.cneicy.shrink-runtime-abstractions",
"version": "0.1.0",
"repository": "https://git.crash.work/ShrinkSDK/ShrinkRuntime.Abstractions.git",
"nugetPackages": [
{
"id": "ShrinkSDK.Runtime.Abstractions",
"version": "0.1.0",
"project": "Assets/Modules/ShrinkRuntime.Abstractions/DotNet~/ShrinkSDK.Runtime.Abstractions.csproj"
}
]
},
{
"directory": "ShrinkShared.CodeGen",
"name": "com.cneicy.shrink-shared-codegen",
"version": "0.1.1",
"repository": "https://git.crash.work/ShrinkSDK/ShrinkShared.CodeGen.git",
"nugetPackages": [
{
"id": "ShrinkSDK.CodeGen",
"version": "0.1.0",
"project": "Assets/Modules/ShrinkShared.CodeGen/DotNet~/ShrinkSDK.CodeGen.Task/ShrinkSDK.CodeGen.Task.csproj"
}
]
},
{
"directory": "ShrinkTutorial",
"name": "com.cneicy.shrink-tutorial",
"version": "0.2.0",
"repository": "https://git.crash.work/ShrinkSDK/ShrinkTutorial.git",
"nugetPackages": [
{
"id": "ShrinkSDK.Tutorial",
"version": "0.2.0",
"project": "Assets/Modules/ShrinkTutorial/DotNet~/ShrinkSDK.Tutorial.csproj"
},
{
"id": "ShrinkSDK.Tutorial.Godot",
"version": "0.1.0",
"project": "Assets/Modules/ShrinkTutorial/Godot~/ShrinkSDK.Tutorial.Godot.csproj"
}
]
}
],
"standaloneNugetPackages": [
{
"id": "ShrinkSDK.Godot",
"version": "0.1.0",
"project": "Godot/Packages/ShrinkSDK.Godot/ShrinkSDK.Godot.csproj",
"repository": "https://git.crash.work/ShrinkSDK/ShrinkGodot.git"
}
]
}
+867
View File
@@ -0,0 +1,867 @@
#!/usr/bin/env node
import fs from 'node:fs';
import path from 'node:path';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
const workspaceRoot = path.resolve(scriptDirectory, '..', '..');
const packageCatalogPath = path.join(scriptDirectory, 'package-versions.json');
const installerPackageName = 'com.cneicy.shrink-installer';
const semanticVersionPattern = '(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?';
const semanticVersion = new RegExp(`^${semanticVersionPattern}$`);
function usage(message) {
if (message) {
console.error(message);
}
console.error('Usage:');
console.error(' node Tools/Release/update-shrinksdk-versions.mjs --check');
console.error(' node Tools/Release/update-shrinksdk-versions.mjs --package <name> --bump <major|minor|patch> [--dry-run]');
console.error(' node Tools/Release/update-shrinksdk-versions.mjs --package <name> --version <x.y.z> [--dry-run]');
process.exitCode = 2;
}
function parseArguments(argv) {
const options = { check: false, dryRun: false, package: null, bump: null, version: null };
for (let index = 0; index < argv.length; index += 1) {
const argument = argv[index];
switch (argument) {
case '--check':
options.check = true;
break;
case '--dry-run':
options.dryRun = true;
break;
case '--package':
options.package = argv[++index] ?? null;
break;
case '--bump':
options.bump = argv[++index] ?? null;
break;
case '--version':
options.version = argv[++index] ?? null;
break;
case '--help':
case '-h':
return { help: true };
default:
throw new Error(`Unknown argument: ${argument}`);
}
}
if (options.check) {
if (options.package || options.bump || options.version || options.dryRun) {
throw new Error('--check cannot be combined with update arguments.');
}
return options;
}
if (!options.package || Boolean(options.bump) === Boolean(options.version)) {
throw new Error('An update requires --package and exactly one of --bump or --version.');
}
if (options.bump && !['major', 'minor', 'patch'].includes(options.bump)) {
throw new Error(`Unsupported bump kind: ${options.bump}`);
}
if (options.version && !semanticVersion.test(options.version)) {
throw new Error(`Invalid semantic version: ${options.version}`);
}
return options;
}
function normalizeRelative(value) {
return value.replaceAll('\\', '/').replace(/^\.\//, '');
}
function relativeToWorkspace(absolutePath) {
return normalizeRelative(path.relative(workspaceRoot, absolutePath));
}
function escapeRegularExpression(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function readGitModules() {
const gitModulesPath = path.join(workspaceRoot, '.gitmodules');
if (!fs.existsSync(gitModulesPath)) {
throw new Error(`Workspace .gitmodules was not found: ${gitModulesPath}`);
}
const modules = [];
let current = null;
for (const line of fs.readFileSync(gitModulesPath, 'utf8').split(/\r?\n/)) {
const section = line.match(/^\s*\[submodule\s+"(.+)"\]\s*$/);
if (section) {
current = { section: section[1], path: null, url: null };
modules.push(current);
continue;
}
if (!current) {
continue;
}
const property = line.match(/^\s*(path|url)\s*=\s*(.*?)\s*$/);
if (property) {
current[property[1]] = property[1] === 'path' ? normalizeRelative(property[2]) : property[2];
}
}
const seenPaths = new Set();
for (const module of modules) {
if (!module.path || !module.url) {
throw new Error(`Incomplete .gitmodules entry: ${module.section}`);
}
const key = module.path.toLowerCase();
if (seenPaths.has(key)) {
throw new Error(`Duplicate .gitmodules path: ${module.path}`);
}
seenPaths.add(key);
}
return modules;
}
function walkFiles(directory, predicate) {
if (!fs.existsSync(directory)) {
return [];
}
const result = [];
const visit = current => {
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
if (entry.name === '.git' || entry.name === 'bin' || entry.name === 'obj' || entry.name === 'Development~') {
continue;
}
const fullPath = path.join(current, entry.name);
if (entry.isDirectory()) {
visit(fullPath);
} else if (entry.isFile() && predicate(fullPath)) {
result.push(fullPath);
}
}
};
visit(directory);
return result.sort((left, right) => left.localeCompare(right));
}
function xmlElement(text, name) {
return text.match(new RegExp(`<${name}>\\s*([^<]+?)\\s*</${name}>`, 'i'))?.[1]?.trim() ?? null;
}
function xmlAttribute(text, name) {
return text.match(new RegExp(`\\b${name}\\s*=\\s*"([^"]+)"`, 'i'))?.[1]?.trim() ?? null;
}
function readPackageReferences(text) {
const references = [];
const elementPattern = /<PackageReference\b[\s\S]*?(?:\/>|<\/PackageReference>)/gi;
for (const match of text.matchAll(elementPattern)) {
const include = xmlAttribute(match[0], 'Include');
const version = xmlAttribute(match[0], 'Version') ?? xmlElement(match[0], 'Version');
if (include && version) {
references.push({ id: include, version });
}
}
return references;
}
function readProject(projectPath, ownerModule, ownerUpmName) {
const text = fs.readFileSync(projectPath, 'utf8');
const id = xmlElement(text, 'PackageId');
const version = xmlElement(text, 'Version');
const isPackable = xmlElement(text, 'IsPackable');
if (!id || !version || String(isPackable).toLowerCase() === 'false') {
return null;
}
if (!semanticVersion.test(version)) {
throw new Error(`${relativeToWorkspace(projectPath)} has an invalid package version: ${version}`);
}
const relativeToOwner = normalizeRelative(path.relative(path.join(workspaceRoot, ownerModule.path), projectPath));
const segments = relativeToOwner.split('/');
return {
id,
version,
path: projectPath,
project: relativeToWorkspace(projectPath),
repository: ownerModule.url,
ownerModulePath: ownerModule.path,
ownerUpmName,
coupledToUpm: Boolean(ownerUpmName) && segments.length === 2 && segments[0] === 'DotNet~',
references: readPackageReferences(text)
};
}
function buildModel() {
const modules = readGitModules();
const upmModules = modules.filter(module => /^Assets\/Modules\/[^/]+$/.test(module.path));
const declaredPaths = new Set(upmModules.map(module => module.path.toLowerCase()));
const modulesDirectory = path.join(workspaceRoot, 'Assets', 'Modules');
const manifestDirectories = fs.readdirSync(modulesDirectory, { withFileTypes: true })
.filter(entry => entry.isDirectory() && fs.existsSync(path.join(modulesDirectory, entry.name, 'package.json')))
.map(entry => `Assets/Modules/${entry.name}`);
const undeclared = manifestDirectories.filter(directory => !declaredPaths.has(directory.toLowerCase()));
if (undeclared.length > 0) {
throw new Error(`Package directories are not declared in .gitmodules: ${undeclared.join(', ')}`);
}
const upmPackages = [];
const upmByName = new Map();
for (const module of upmModules) {
const packagePath = path.join(workspaceRoot, module.path);
const manifestPath = path.join(packagePath, 'package.json');
if (!fs.existsSync(manifestPath)) {
throw new Error(`Declared package submodule has no package.json: ${module.path}`);
}
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
if (typeof manifest.name !== 'string' || typeof manifest.version !== 'string' || !semanticVersion.test(manifest.version)) {
throw new Error(`${module.path}/package.json has no valid name and semantic version.`);
}
if (upmByName.has(manifest.name)) {
throw new Error(`Duplicate UPM package name: ${manifest.name}`);
}
const dependencies = Object.entries(manifest.dependencies ?? {})
.filter(([name]) => name.startsWith('com.cneicy.'))
.map(([name, version]) => ({ name, version: String(version) }));
const record = {
name: manifest.name,
displayName: typeof manifest.displayName === 'string' ? manifest.displayName : '',
version: manifest.version,
directory: path.posix.basename(module.path),
modulePath: module.path,
packagePath,
manifestPath,
repository: module.url,
dependencies,
nugetPackages: []
};
upmPackages.push(record);
upmByName.set(record.name, record);
}
const nugetPackages = [];
const nugetById = new Map();
const addProject = record => {
if (!record) {
return;
}
if (nugetById.has(record.id)) {
throw new Error(`Duplicate NuGet package id: ${record.id}`);
}
nugetPackages.push(record);
nugetById.set(record.id, record);
if (record.ownerUpmName) {
upmByName.get(record.ownerUpmName).nugetPackages.push(record);
}
};
for (const upmPackage of upmPackages) {
const ownerModule = upmModules.find(module => module.path === upmPackage.modulePath);
for (const area of ['DotNet~', 'Godot~']) {
const areaPath = path.join(upmPackage.packagePath, area);
for (const projectPath of walkFiles(areaPath, candidate => candidate.toLowerCase().endsWith('.csproj'))) {
addProject(readProject(projectPath, ownerModule, upmPackage.name));
}
}
}
for (const module of modules.filter(candidate => !declaredPaths.has(candidate.path.toLowerCase()))) {
const moduleRoot = path.join(workspaceRoot, module.path);
for (const projectPath of walkFiles(moduleRoot, candidate => candidate.toLowerCase().endsWith('.csproj'))) {
addProject(readProject(projectPath, module, null));
}
}
for (const upmPackage of upmPackages) {
const coupled = upmPackage.nugetPackages.filter(project => project.coupledToUpm);
if (coupled.length > 1) {
throw new Error(`${upmPackage.directory} has more than one directly coupled DotNet~ project.`);
}
}
const installer = upmByName.get(installerPackageName);
if (!installer) {
throw new Error(`Installer package was not found: ${installerPackageName}`);
}
const installerUpmCatalogPath = path.join(installer.packagePath, 'Editor', 'ShrinkSdkPackageCatalog.cs');
const installerNugetCatalogPath = path.join(installer.packagePath, 'Godot~', 'addons', 'shrinksdk', 'ShrinkProjectPackageEditor.cs');
return {
modules,
upmPackages: upmPackages.sort((left, right) => left.name.localeCompare(right.name)),
upmByName,
nugetPackages: nugetPackages.sort((left, right) => left.id.localeCompare(right.id)),
nugetById,
installer,
installerUpmCatalogPath,
installerNugetCatalogPath,
designPath: path.join(workspaceRoot, 'DESIGN.md')
};
}
function parseVersion(value) {
const match = value.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/);
if (!match) {
throw new Error(`Cannot parse semantic version: ${value}`);
}
return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]), prerelease: match[4] ?? null };
}
function compareVersions(leftValue, rightValue) {
const left = parseVersion(leftValue);
const right = parseVersion(rightValue);
for (const key of ['major', 'minor', 'patch']) {
if (left[key] !== right[key]) {
return left[key] < right[key] ? -1 : 1;
}
}
if (left.prerelease === right.prerelease) {
return 0;
}
if (left.prerelease === null) {
return 1;
}
if (right.prerelease === null) {
return -1;
}
const leftParts = left.prerelease.split('.');
const rightParts = right.prerelease.split('.');
const length = Math.max(leftParts.length, rightParts.length);
for (let index = 0; index < length; index += 1) {
if (leftParts[index] === undefined) return -1;
if (rightParts[index] === undefined) return 1;
if (leftParts[index] === rightParts[index]) continue;
const leftNumeric = /^[0-9]+$/.test(leftParts[index]);
const rightNumeric = /^[0-9]+$/.test(rightParts[index]);
if (leftNumeric && rightNumeric) return Number(leftParts[index]) < Number(rightParts[index]) ? -1 : 1;
if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1;
return leftParts[index].localeCompare(rightParts[index]);
}
return 0;
}
function bumpVersion(value, kind) {
const parsed = parseVersion(value);
if (parsed.prerelease) {
throw new Error(`Automatic ${kind} bump requires a stable version: ${value}`);
}
if (kind === 'major') return `${parsed.major + 1}.0.0`;
if (kind === 'minor') return `${parsed.major}.${parsed.minor + 1}.0`;
return `${parsed.major}.${parsed.minor}.${parsed.patch + 1}`;
}
function parseUpmCatalog(model) {
const text = fs.readFileSync(model.installerUpmCatalogPath, 'utf8');
const entries = new Map();
const pattern = new RegExp(`"(com\\.cneicy\\.[^"]+)"\\s*,\\s*"(${semanticVersionPattern})"\\s*,\\s*ShrinkSdkPackageLayer`, 'g');
for (const match of text.matchAll(pattern)) {
if (entries.has(match[1])) throw new Error(`Duplicate Installer UPM catalog entry: ${match[1]}`);
entries.set(match[1], match[2]);
}
return entries;
}
function parseNugetCatalog(model) {
const text = fs.readFileSync(model.installerNugetCatalogPath, 'utf8');
const entries = new Map();
const pattern = new RegExp(`\\["(ShrinkSDK\\.[^"]+)"\\]\\s*=\\s*"(${semanticVersionPattern})"`, 'g');
for (const match of text.matchAll(pattern)) {
if (entries.has(match[1])) throw new Error(`Duplicate Installer NuGet catalog entry: ${match[1]}`);
entries.set(match[1], match[2]);
}
return entries;
}
function parseDesignVersions(model) {
const text = fs.readFileSync(model.designPath, 'utf8');
const entries = new Map();
const pattern = new RegExp('^\\|\\s*`(com\\.cneicy\\.[^`]+)`\\s*\\|\\s*(' + semanticVersionPattern + ')\\s*\\|', 'gm');
for (const match of text.matchAll(pattern)) {
if (entries.has(match[1])) throw new Error(`Duplicate DESIGN.md package row: ${match[1]}`);
entries.set(match[1], match[2]);
}
return entries;
}
function finalUpmVersion(record, upmPlan = new Map()) {
return upmPlan.get(record.name) ?? record.version;
}
function finalNugetVersion(record, nugetPlan = new Map()) {
return nugetPlan.get(record.id) ?? record.version;
}
function generatePackageCatalog(model, upmPlan = new Map(), nugetPlan = new Map()) {
const unityPackages = model.upmPackages.map(record => {
const entry = {
directory: record.directory,
name: record.name,
version: finalUpmVersion(record, upmPlan),
repository: record.repository
};
if (record.nugetPackages.length > 0) {
entry.nugetPackages = record.nugetPackages
.map(project => ({ id: project.id, version: finalNugetVersion(project, nugetPlan), project: project.project }))
.sort((left, right) => left.id.localeCompare(right.id));
}
return entry;
});
const standaloneNugetPackages = model.nugetPackages
.filter(record => !record.ownerUpmName)
.map(record => ({
id: record.id,
version: finalNugetVersion(record, nugetPlan),
project: record.project,
repository: record.repository
}));
return `${JSON.stringify({ schemaVersion: 1, unityPackages, standaloneNugetPackages }, null, 2)}\n`;
}
function collectValidationErrors(model) {
const errors = [];
const upmCatalog = parseUpmCatalog(model);
const nugetCatalog = parseNugetCatalog(model);
const designVersions = parseDesignVersions(model);
for (const record of model.upmPackages) {
for (const dependency of record.dependencies) {
const target = model.upmByName.get(dependency.name);
if (!target) continue;
if (dependency.version !== target.version) {
errors.push(`${record.name} requires ${dependency.name} ${dependency.version}, Workspace has ${target.version}`);
}
const canDependOnIntegration = record.name.includes('-integration-') || record.name.includes('-starter-');
if (dependency.name.includes('-integration-') && !canDependOnIntegration) {
errors.push(`${record.name} must not depend on integration package ${dependency.name}`);
}
}
const coupled = record.nugetPackages.filter(project => project.coupledToUpm);
for (const project of coupled) {
if (project.version !== record.version) {
errors.push(`${project.project} version ${project.version} differs from ${record.name} ${record.version}`);
}
}
const designVersion = designVersions.get(record.name);
if (!designVersion) {
errors.push(`DESIGN.md is missing package ${record.name}`);
} else if (designVersion !== record.version) {
errors.push(`DESIGN.md lists ${record.name} ${designVersion}, Workspace has ${record.version}`);
}
}
const expectedCatalogNames = new Set(model.upmPackages
.filter(record => record.name !== installerPackageName)
.map(record => record.name));
for (const name of expectedCatalogNames) {
const record = model.upmByName.get(name);
if (!upmCatalog.has(name)) {
errors.push(`Installer UPM catalog is missing ${name}`);
} else if (upmCatalog.get(name) !== record.version) {
errors.push(`Installer UPM catalog lists ${name} ${upmCatalog.get(name)}, Workspace has ${record.version}`);
}
}
for (const name of upmCatalog.keys()) {
if (!expectedCatalogNames.has(name)) errors.push(`Installer UPM catalog has unknown package ${name}`);
}
for (const [id, version] of nugetCatalog) {
const record = model.nugetById.get(id);
if (!record) {
errors.push(`Installer NuGet catalog has unknown package ${id}`);
} else if (version !== record.version) {
errors.push(`Installer NuGet catalog lists ${id} ${version}, project has ${record.version}`);
}
}
for (const project of model.nugetPackages) {
for (const reference of project.references) {
const target = model.nugetById.get(reference.id);
if (target && reference.version !== target.version) {
errors.push(`${project.project} references ${reference.id} ${reference.version}, Workspace has ${target.version}`);
}
}
}
const dependencies = new Map(model.upmPackages.map(record => [
record.name,
new Set(record.dependencies.filter(dependency => model.upmByName.has(dependency.name)).map(dependency => dependency.name))
]));
const resolved = new Set();
let progress = true;
while (progress) {
progress = false;
for (const [name, values] of dependencies) {
if (!resolved.has(name) && [...values].every(dependency => resolved.has(dependency))) {
resolved.add(name);
progress = true;
}
}
}
if (resolved.size !== model.upmPackages.length) {
const blocked = [...dependencies]
.filter(([name]) => !resolved.has(name))
.map(([name, values]) => `${name} -> ${[...values].filter(value => !resolved.has(value)).join(', ')}`);
errors.push(`Circular UPM dependencies: ${blocked.join('; ')}`);
}
const expectedCatalog = generatePackageCatalog(model);
if (!fs.existsSync(packageCatalogPath)) {
errors.push(`Generated package catalog is missing: ${relativeToWorkspace(packageCatalogPath)}`);
} else {
const actualCatalog = fs.readFileSync(packageCatalogPath, 'utf8').replaceAll('\r\n', '\n');
if (actualCatalog !== expectedCatalog) {
errors.push(`${relativeToWorkspace(packageCatalogPath)} is out of date`);
}
}
return errors;
}
function resolveTarget(model, requested) {
const key = requested.toLowerCase();
const upmMatches = model.upmPackages.filter(record =>
[record.name, record.directory, record.displayName].some(value => value && value.toLowerCase() === key));
const nugetMatches = model.nugetPackages.filter(record => record.id.toLowerCase() === key);
const matches = [...upmMatches.map(record => ({ kind: 'upm', record })), ...nugetMatches.map(record => ({ kind: 'nuget', record }))];
if (matches.length === 0) {
throw new Error(`Package was not found: ${requested}`);
}
if (matches.length > 1) {
throw new Error(`Package name is ambiguous: ${requested}`);
}
return matches[0];
}
function createPlan(model, options) {
const upmPlan = new Map();
const nugetPlan = new Map();
const reasons = new Map();
const reasonKey = (kind, name) => `${kind}:${name}`;
const addReason = (kind, name, reason) => {
const key = reasonKey(kind, name);
const values = reasons.get(key) ?? [];
if (!values.includes(reason)) values.push(reason);
reasons.set(key, values);
};
const setUpm = (record, version, reason) => {
const existing = upmPlan.get(record.name);
if (existing && existing !== version) throw new Error(`Conflicting versions planned for ${record.name}: ${existing} and ${version}`);
addReason('upm', record.name, reason);
if (existing) return false;
upmPlan.set(record.name, version);
return true;
};
const setNuget = (record, version, reason) => {
const existing = nugetPlan.get(record.id);
if (existing && existing !== version) throw new Error(`Conflicting versions planned for ${record.id}: ${existing} and ${version}`);
addReason('nuget', record.id, reason);
if (existing) return false;
nugetPlan.set(record.id, version);
return true;
};
const ensureUpmPatch = (record, reason) => setUpm(record, bumpVersion(record.version, 'patch'), reason);
const ensureNugetPatch = (record, reason) => {
if (record.coupledToUpm) return ensureUpmPatch(model.upmByName.get(record.ownerUpmName), reason);
const changed = setNuget(record, bumpVersion(record.version, 'patch'), reason);
if (record.ownerUpmName) ensureUpmPatch(model.upmByName.get(record.ownerUpmName), `${record.id} content changed`);
return changed;
};
const target = resolveTarget(model, options.package);
const currentVersion = target.record.version;
const requestedVersion = options.version ?? bumpVersion(currentVersion, options.bump);
if (compareVersions(requestedVersion, currentVersion) <= 0) {
throw new Error(`Requested version must be newer than ${currentVersion}: ${requestedVersion}`);
}
if (target.kind === 'upm') {
setUpm(target.record, requestedVersion, 'requested update');
} else if (target.record.coupledToUpm) {
setUpm(model.upmByName.get(target.record.ownerUpmName), requestedVersion, `requested through ${target.record.id}`);
} else {
setNuget(target.record, requestedVersion, 'requested update');
if (target.record.ownerUpmName) {
ensureUpmPatch(model.upmByName.get(target.record.ownerUpmName), `${target.record.id} content changed`);
}
}
for (const record of model.upmPackages) {
for (const dependency of record.dependencies) {
const targetRecord = model.upmByName.get(dependency.name);
if (targetRecord && dependency.version !== targetRecord.version) {
ensureUpmPatch(record, `repair ${dependency.name} dependency`);
}
}
for (const project of record.nugetPackages.filter(candidate => candidate.coupledToUpm)) {
if (project.version !== record.version) ensureUpmPatch(record, `repair ${project.id} version`);
}
}
for (const project of model.nugetPackages) {
for (const reference of project.references) {
const targetRecord = model.nugetById.get(reference.id);
if (targetRecord && reference.version !== targetRecord.version) {
ensureNugetPatch(project, `repair ${reference.id} reference`);
}
}
}
const upmCatalog = parseUpmCatalog(model);
const nugetCatalog = parseNugetCatalog(model);
let changed = true;
while (changed) {
changed = false;
for (const record of model.upmPackages) {
if (!upmPlan.has(record.name)) continue;
for (const project of record.nugetPackages.filter(candidate => candidate.coupledToUpm)) {
changed = setNuget(project, upmPlan.get(record.name), `coupled to ${record.name}`) || changed;
}
}
for (const project of model.nugetPackages.filter(candidate => candidate.coupledToUpm && nugetPlan.has(candidate.id))) {
changed = setUpm(model.upmByName.get(project.ownerUpmName), nugetPlan.get(project.id), `coupled to ${project.id}`) || changed;
}
for (const record of model.upmPackages) {
for (const dependency of record.dependencies) {
const targetRecord = model.upmByName.get(dependency.name);
if (!targetRecord) continue;
const targetVersion = finalUpmVersion(targetRecord, upmPlan);
if (dependency.version !== targetVersion) {
changed = ensureUpmPatch(record, `${dependency.name} becomes ${targetVersion}`) || changed;
}
}
}
for (const project of model.nugetPackages) {
for (const reference of project.references) {
const targetRecord = model.nugetById.get(reference.id);
if (!targetRecord) continue;
const targetVersion = finalNugetVersion(targetRecord, nugetPlan);
if (reference.version !== targetVersion) {
changed = ensureNugetPatch(project, `${reference.id} becomes ${targetVersion}`) || changed;
}
}
}
for (const project of model.nugetPackages.filter(candidate => nugetPlan.has(candidate.id) && candidate.ownerUpmName && !candidate.coupledToUpm)) {
changed = ensureUpmPatch(model.upmByName.get(project.ownerUpmName), `${project.id} content changed`) || changed;
}
for (const record of model.upmPackages.filter(candidate => candidate.name !== installerPackageName)) {
if (upmCatalog.get(record.name) !== finalUpmVersion(record, upmPlan)) {
changed = ensureUpmPatch(model.installer, `${record.name} catalog changed`) || changed;
}
}
for (const [id, catalogVersion] of nugetCatalog) {
const project = model.nugetById.get(id);
if (project && catalogVersion !== finalNugetVersion(project, nugetPlan)) {
changed = ensureUpmPatch(model.installer, `${id} catalog changed`) || changed;
}
}
}
return { upmPlan, nugetPlan, reasons };
}
function replaceSingle(text, pattern, replacer, description) {
const countPattern = new RegExp(pattern.source, pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`);
const matches = [...text.matchAll(countPattern)];
if (matches.length !== 1) {
throw new Error(`${description}: expected one match, found ${matches.length}`);
}
return text.replace(pattern, replacer);
}
function replaceManifestVersion(text, version, manifestPath) {
return replaceSingle(
text,
/^(\s*"version"\s*:\s*")([^"]+)(")/m,
`$1${version}$3`,
`${relativeToWorkspace(manifestPath)} top-level version`
);
}
function replaceJsonValue(text, name, version, filePath) {
const pattern = new RegExp(`("${escapeRegularExpression(name)}"\\s*:\\s*")([^"]+)(")`, 'g');
return replaceSingle(text, pattern, `$1${version}$3`, `${relativeToWorkspace(filePath)} ${name}`);
}
function replaceProjectVersion(text, version, projectPath) {
return replaceSingle(
text,
/(<Version>\s*)([^<]+?)(\s*<\/Version>)/i,
`$1${version}$3`,
`${relativeToWorkspace(projectPath)} Version`
);
}
function replacePackageReferenceVersion(text, id, version, projectPath) {
const elementPattern = /<PackageReference\b[\s\S]*?(?:\/>|<\/PackageReference>)/gi;
const elements = [...text.matchAll(elementPattern)].filter(match => xmlAttribute(match[0], 'Include') === id);
if (elements.length !== 1) {
throw new Error(`${relativeToWorkspace(projectPath)} ${id} reference: expected one match, found ${elements.length}`);
}
const original = elements[0][0];
let replacement;
if (/\bVersion\s*=\s*"[^"]+"/i.test(original)) {
replacement = original.replace(/(\bVersion\s*=\s*")([^"]+)(")/i, `$1${version}$3`);
} else {
replacement = replaceSingle(original, /(<Version>\s*)([^<]+?)(\s*<\/Version>)/i, `$1${version}$3`, `${id} nested Version`);
}
return `${text.slice(0, elements[0].index)}${replacement}${text.slice(elements[0].index + original.length)}`;
}
function buildUpdatedContents(model, plan) {
const contents = new Map();
const read = filePath => contents.get(filePath) ?? fs.readFileSync(filePath, 'utf8');
const write = (filePath, text) => contents.set(filePath, text);
for (const record of model.upmPackages.filter(candidate => plan.upmPlan.has(candidate.name))) {
let text = read(record.manifestPath);
text = replaceManifestVersion(text, plan.upmPlan.get(record.name), record.manifestPath);
for (const dependency of record.dependencies) {
const target = model.upmByName.get(dependency.name);
if (!target) continue;
const targetVersion = finalUpmVersion(target, plan.upmPlan);
if (dependency.version !== targetVersion) {
text = replaceJsonValue(text, dependency.name, targetVersion, record.manifestPath);
}
}
write(record.manifestPath, text);
}
for (const project of model.nugetPackages.filter(candidate => plan.nugetPlan.has(candidate.id))) {
let text = read(project.path);
text = replaceProjectVersion(text, plan.nugetPlan.get(project.id), project.path);
for (const reference of project.references) {
const target = model.nugetById.get(reference.id);
if (!target) continue;
const targetVersion = finalNugetVersion(target, plan.nugetPlan);
if (reference.version !== targetVersion) {
text = replacePackageReferenceVersion(text, reference.id, targetVersion, project.path);
}
}
write(project.path, text);
}
let upmCatalogText = read(model.installerUpmCatalogPath);
const upmCatalog = parseUpmCatalog(model);
for (const record of model.upmPackages.filter(candidate => candidate.name !== installerPackageName)) {
const version = finalUpmVersion(record, plan.upmPlan);
if (upmCatalog.get(record.name) !== version) {
const pattern = new RegExp(`("${escapeRegularExpression(record.name)}"\\s*,\\s*")([^"]+)(")`, 'g');
upmCatalogText = replaceSingle(upmCatalogText, pattern, `$1${version}$3`, `Installer UPM catalog ${record.name}`);
}
}
write(model.installerUpmCatalogPath, upmCatalogText);
let nugetCatalogText = read(model.installerNugetCatalogPath);
const nugetCatalog = parseNugetCatalog(model);
for (const [id, currentVersion] of nugetCatalog) {
const project = model.nugetById.get(id);
if (!project) continue;
const version = finalNugetVersion(project, plan.nugetPlan);
if (currentVersion !== version) {
const pattern = new RegExp(`(\\["${escapeRegularExpression(id)}"\\]\\s*=\\s*")([^"]+)(")`, 'g');
nugetCatalogText = replaceSingle(nugetCatalogText, pattern, `$1${version}$3`, `Installer NuGet catalog ${id}`);
}
}
write(model.installerNugetCatalogPath, nugetCatalogText);
let designText = read(model.designPath);
const designVersions = parseDesignVersions(model);
for (const record of model.upmPackages) {
const version = finalUpmVersion(record, plan.upmPlan);
if (!designVersions.has(record.name)) {
throw new Error(`DESIGN.md is missing package ${record.name}`);
}
if (designVersions.get(record.name) !== version) {
const pattern = new RegExp('^(\\|\\s*`' + escapeRegularExpression(record.name) + '`\\s*\\|\\s*)[^|]+?(\\s*\\|)', 'm');
designText = replaceSingle(designText, pattern, `$1${version}$2`, `DESIGN.md ${record.name}`);
}
}
write(model.designPath, designText);
write(packageCatalogPath, generatePackageCatalog(model, plan.upmPlan, plan.nugetPlan));
for (const [filePath, text] of [...contents]) {
if (fs.existsSync(filePath) && fs.readFileSync(filePath, 'utf8') === text) {
contents.delete(filePath);
}
}
return contents;
}
function printPlan(model, plan, contents, dryRun) {
console.log(dryRun ? 'Dry-run version plan:' : 'Applied version plan:');
for (const record of model.upmPackages.filter(candidate => plan.upmPlan.has(candidate.name))) {
const reasons = plan.reasons.get(`upm:${record.name}`) ?? [];
console.log(` UPM ${record.name}: ${record.version} -> ${plan.upmPlan.get(record.name)} (${reasons.join('; ')})`);
}
for (const record of model.nugetPackages.filter(candidate => plan.nugetPlan.has(candidate.id))) {
const reasons = plan.reasons.get(`nuget:${record.id}`) ?? [];
console.log(` NuGet ${record.id}: ${record.version} -> ${plan.nugetPlan.get(record.id)} (${reasons.join('; ')})`);
}
console.log('Files:');
for (const filePath of [...contents.keys()].sort((left, right) => left.localeCompare(right))) {
console.log(` ${relativeToWorkspace(filePath)}`);
}
}
function applyContents(contents) {
const originals = new Map();
try {
for (const [filePath, text] of contents) {
originals.set(filePath, fs.existsSync(filePath) ? fs.readFileSync(filePath) : null);
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, text, 'utf8');
}
const refreshed = buildModel();
const errors = collectValidationErrors(refreshed);
if (errors.length > 0) {
throw new Error(`Post-update validation failed:\n${errors.map(error => ` - ${error}`).join('\n')}`);
}
} catch (error) {
for (const [filePath, original] of originals) {
if (original === null) fs.rmSync(filePath, { force: true });
else fs.writeFileSync(filePath, original);
}
throw error;
}
}
function main() {
let options;
try {
options = parseArguments(process.argv.slice(2));
} catch (error) {
usage(error.message);
return;
}
if (options.help) {
usage();
process.exitCode = 0;
return;
}
try {
const model = buildModel();
if (options.check) {
const errors = collectValidationErrors(model);
if (errors.length > 0) {
throw new Error(`ShrinkSDK version validation failed:\n${errors.map(error => ` - ${error}`).join('\n')}`);
}
console.log(`ShrinkSDK version validation passed: UPM=${model.upmPackages.length}, NuGet=${model.nugetPackages.length}`);
return;
}
const plan = createPlan(model, options);
const contents = buildUpdatedContents(model, plan);
printPlan(model, plan, contents, options.dryRun);
if (!options.dryRun) {
applyContents(contents);
}
} catch (error) {
console.error(error.stack ?? error.message);
process.exitCode = 1;
}
}
main();
@@ -38,6 +38,26 @@ $PackageDirectories = @(
'ShrinkTutorial' 'ShrinkTutorial'
) )
$DevelopmentHostOnlyDependencies = @{
'com.cneicy.shrink-command-integration-network' = [ordered]@{
'com.cneicy.shrink-command-integration-app' = '0.1.3'
'com.cneicy.shrink-datasaver-integration-app' = '0.1.3'
'com.cneicy.shrink-network-integration-app' = '0.1.3'
}
'com.cneicy.shrink-context-app-adapter' = [ordered]@{
'com.cneicy.shrink-app-starter-basic' = '0.2.2'
'com.cneicy.shrink-command' = '0.2.0'
'com.cneicy.shrink-command-integration-app' = '0.1.3'
'com.cneicy.shrink-datasaver-integration-app' = '0.1.3'
'com.cneicy.shrink-network' = '0.2.1'
'com.cneicy.shrink-network-integration-app' = '0.1.3'
}
'com.cneicy.shrink-eventbus-entities' = [ordered]@{
'com.unity.modules.physics' = '1.0.0'
'com.unity.modules.uielements' = '1.0.0'
}
}
function Set-JsonProperty { function Set-JsonProperty {
param( param(
[Parameter(Mandatory)] [object]$Target, [Parameter(Mandatory)] [object]$Target,
@@ -240,12 +260,17 @@ jobs:
shell: bash shell: bash
run: | run: |
set -eu set -eu
machine_id_file="/root/.local/share/unity3d/Unity/.machine-id"
if test -s "$machine_id_file"; then
cat "$machine_id_file" > /etc/machine-id
echo "Unity machine identity restored"
fi
git config --global url."https://ghfast.top/https://github.com/".insteadOf "https://github.com/"
unity_bin="$(command -v unity-editor || command -v unity || command -v Unity || true)" unity_bin="$(command -v unity-editor || command -v unity || command -v Unity || true)"
test -n "$unity_bin" test -n "$unity_bin"
"$unity_bin" \ "$unity_bin" \
-batchmode \ -batchmode \
-nographics \ -nographics \
-quit \
-projectPath "$PWD/Development~/UnityProject" \ -projectPath "$PWD/Development~/UnityProject" \
-runTests \ -runTests \
-testPlatform EditMode \ -testPlatform EditMode \
@@ -255,6 +280,25 @@ jobs:
$verifyWorkflow = $verifyWorkflow.Replace('__REPOSITORY_NAME__', $RepositoryName) $verifyWorkflow = $verifyWorkflow.Replace('__REPOSITORY_NAME__', $RepositoryName)
Write-Utf8File -Path (Join-Path $PackageRoot '.gitea/workflows/unity-verify.yml') -Content $verifyWorkflow Write-Utf8File -Path (Join-Path $PackageRoot '.gitea/workflows/unity-verify.yml') -Content $verifyWorkflow
$package = Get-Content -LiteralPath (Join-Path $PackageRoot 'package.json') -Raw | ConvertFrom-Json
$dependencies = [ordered]@{
'com.unity.test-framework' = '1.1.33'
}
if ($package.dependencies) {
foreach ($dependency in $package.dependencies.PSObject.Properties) {
if ($dependency.Name -ne 'com.cysharp.unitask') {
$dependencies[$dependency.Name] = [string]$dependency.Value
}
}
}
$dependencies[$PackageName] = 'file:../../..'
if ($DevelopmentHostOnlyDependencies.ContainsKey($PackageName)) {
foreach ($dependency in $DevelopmentHostOnlyDependencies[$PackageName].GetEnumerator()) {
$dependencies[$dependency.Key] = $dependency.Value
}
}
$dependencies['com.cysharp.unitask'] = 'https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask#7c0f199fe0d3fc528024488ccd671e6c7b27745b'
$manifest = [ordered]@{ $manifest = [ordered]@{
scopedRegistries = @( scopedRegistries = @(
[ordered]@{ [ordered]@{
@@ -263,10 +307,8 @@ jobs:
scopes = @($RegistryScope) scopes = @($RegistryScope)
} }
) )
dependencies = [ordered]@{ dependencies = $dependencies
'com.unity.test-framework' = '1.1.33' testables = @($PackageName)
$PackageName = 'file:../../..'
}
} }
$developmentRoot = Join-Path $PackageRoot 'Development~/UnityProject' $developmentRoot = Join-Path $PackageRoot 'Development~/UnityProject'
Write-Utf8File -Path (Join-Path $developmentRoot 'Packages/manifest.json') -Content (($manifest | ConvertTo-Json -Depth 12) + [Environment]::NewLine) Write-Utf8File -Path (Join-Path $developmentRoot 'Packages/manifest.json') -Content (($manifest | ConvertTo-Json -Depth 12) + [Environment]::NewLine)
@@ -0,0 +1,400 @@
#!/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<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."
@@ -179,7 +179,7 @@ if (Test-Path -LiteralPath $consumerRoot) {
New-Item -ItemType Directory -Path $packagesPath, $assetsEditorPath, $projectSettingsPath -Force | Out-Null New-Item -ItemType Directory -Path $packagesPath, $assetsEditorPath, $projectSettingsPath -Force | Out-Null
$dependencies = [ordered]@{ $dependencies = [ordered]@{
'com.cysharp.unitask' = 'https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask' 'com.cysharp.unitask' = 'https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask#7c0f199fe0d3fc528024488ccd671e6c7b27745b'
'com.unity.nuget.newtonsoft-json' = '3.2.2' 'com.unity.nuget.newtonsoft-json' = '3.2.2'
'com.unity.test-framework' = '1.1.33' 'com.unity.test-framework' = '1.1.33'
'com.unity.textmeshpro' = '3.0.7' 'com.unity.textmeshpro' = '3.0.7'
@@ -202,6 +202,7 @@ Copy-Item -LiteralPath (Join-Path $script:ResolvedProjectRoot 'ProjectSettings/P
$expectedPackages = ($packageRecords.Name | Sort-Object | ForEach-Object { Convert-ToCSharpLiteral $_ }) -join ",`n " $expectedPackages = ($packageRecords.Name | Sort-Object | ForEach-Object { Convert-ToCSharpLiteral $_ }) -join ",`n "
$expectedAssemblies = $packageRecords | $expectedAssemblies = $packageRecords |
ForEach-Object { Get-ChildItem -LiteralPath $_.Directory -Recurse -Filter '*.asmdef' -File } | ForEach-Object { Get-ChildItem -LiteralPath $_.Directory -Recurse -Filter '*.asmdef' -File } |
Where-Object { $_.FullName -notmatch '[\\/][^\\/]+~[\\/]' } |
ForEach-Object { (Get-Content -Raw -LiteralPath $_.FullName | ConvertFrom-Json).name } | ForEach-Object { (Get-Content -Raw -LiteralPath $_.FullName | ConvertFrom-Json).name } |
Sort-Object -Unique | Sort-Object -Unique |
ForEach-Object { Convert-ToCSharpLiteral $_ } ForEach-Object { Convert-ToCSharpLiteral $_ }
@@ -0,0 +1,31 @@
{
"schemaVersion": 1,
"packages": {
"com.cneicy.shrink-app-starter-basic": {
"version": "0.3.0",
"assembly": "ShrinkApp.Starter.Basic.Runtime"
},
"com.cneicy.shrink-context-app-adapter": {
"version": "0.2.0",
"assembly": "ShrinkContext.AppAdapter.Runtime"
},
"com.cneicy.shrink-context-eventbus-adapter": {
"version": "0.2.0",
"assembly": "ShrinkContext.EventBusAdapter.Runtime"
},
"com.cneicy.shrink-mod-framework": {
"version": "0.3.0",
"assembly": "ShrinkModFramework.Runtime",
"repository": "https://git.crash.work/ShrinkSDK/ShrinkModFramework.git"
},
"com.cneicy.shrink-tutorial": {
"version": "0.2.0",
"assembly": "ShrinkTutorial.Runtime"
},
"com.cneicy.shrink-installer": {
"version": "0.2.5",
"assembly": "ShrinkInstaller.Editor",
"repository": "https://git.crash.work/ShrinkSDK/Installer.git"
}
}
}