From 0649b1376ea3f0b8c270f9726175927cc753231e Mon Sep 17 00:00:00 2001 From: Vladislav Panin Date: Sat, 24 Oct 2026 18:13:10 +0300 Subject: [PATCH 1/4] chore(build): add the player build check project assets Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01T2kWtnmRdgnk2YsZd4MdvY --- .../Aspid.FastTools.PlayerBuildCheck.asmdef | 17 ++ ...pid.FastTools.PlayerBuildCheck.asmdef.meta | 7 + .../Assets/PlayerBuildCheck~/Editor.meta | 8 + ...d.FastTools.PlayerBuildCheck.Editor.asmdef | 20 +++ ...tTools.PlayerBuildCheck.Editor.asmdef.meta | 7 + .../PlayerBuildCheck~/Editor/PlayerBuilds.cs | 167 ++++++++++++++++++ .../Editor/PlayerBuilds.cs.meta | 11 ++ .../PlayerBuildCheck~/PlayerBuildProbe.cs | 95 ++++++++++ .../PlayerBuildProbe.cs.meta | 11 ++ .../PlayerBuildProbePreserved.cs | 8 + .../PlayerBuildProbePreserved.cs.meta | 11 ++ .../PlayerBuildProbeTarget.cs | 6 + .../PlayerBuildProbeTarget.cs.meta | 11 ++ 13 files changed, 379 insertions(+) create mode 100644 Aspid.FastTools/Assets/PlayerBuildCheck~/Aspid.FastTools.PlayerBuildCheck.asmdef create mode 100644 Aspid.FastTools/Assets/PlayerBuildCheck~/Aspid.FastTools.PlayerBuildCheck.asmdef.meta create mode 100644 Aspid.FastTools/Assets/PlayerBuildCheck~/Editor.meta create mode 100644 Aspid.FastTools/Assets/PlayerBuildCheck~/Editor/Aspid.FastTools.PlayerBuildCheck.Editor.asmdef create mode 100644 Aspid.FastTools/Assets/PlayerBuildCheck~/Editor/Aspid.FastTools.PlayerBuildCheck.Editor.asmdef.meta create mode 100644 Aspid.FastTools/Assets/PlayerBuildCheck~/Editor/PlayerBuilds.cs create mode 100644 Aspid.FastTools/Assets/PlayerBuildCheck~/Editor/PlayerBuilds.cs.meta create mode 100644 Aspid.FastTools/Assets/PlayerBuildCheck~/PlayerBuildProbe.cs create mode 100644 Aspid.FastTools/Assets/PlayerBuildCheck~/PlayerBuildProbe.cs.meta create mode 100644 Aspid.FastTools/Assets/PlayerBuildCheck~/PlayerBuildProbePreserved.cs create mode 100644 Aspid.FastTools/Assets/PlayerBuildCheck~/PlayerBuildProbePreserved.cs.meta create mode 100644 Aspid.FastTools/Assets/PlayerBuildCheck~/PlayerBuildProbeTarget.cs create mode 100644 Aspid.FastTools/Assets/PlayerBuildCheck~/PlayerBuildProbeTarget.cs.meta diff --git a/Aspid.FastTools/Assets/PlayerBuildCheck~/Aspid.FastTools.PlayerBuildCheck.asmdef b/Aspid.FastTools/Assets/PlayerBuildCheck~/Aspid.FastTools.PlayerBuildCheck.asmdef new file mode 100644 index 00000000..ee87083e --- /dev/null +++ b/Aspid.FastTools/Assets/PlayerBuildCheck~/Aspid.FastTools.PlayerBuildCheck.asmdef @@ -0,0 +1,17 @@ +{ + "name": "Aspid.FastTools.PlayerBuildCheck", + "rootNamespace": "Aspid.FastTools.PlayerBuildCheck", + "references": [ + "Aspid.FastTools", + "Aspid.FastTools.Samples.Types" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": false, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/Aspid.FastTools/Assets/PlayerBuildCheck~/Aspid.FastTools.PlayerBuildCheck.asmdef.meta b/Aspid.FastTools/Assets/PlayerBuildCheck~/Aspid.FastTools.PlayerBuildCheck.asmdef.meta new file mode 100644 index 00000000..55623090 --- /dev/null +++ b/Aspid.FastTools/Assets/PlayerBuildCheck~/Aspid.FastTools.PlayerBuildCheck.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 11aced7a945040c98d62f4e64bc28a14 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Assets/PlayerBuildCheck~/Editor.meta b/Aspid.FastTools/Assets/PlayerBuildCheck~/Editor.meta new file mode 100644 index 00000000..6d6d71ab --- /dev/null +++ b/Aspid.FastTools/Assets/PlayerBuildCheck~/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0025257b0b62400ea72b176104fe2261 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Assets/PlayerBuildCheck~/Editor/Aspid.FastTools.PlayerBuildCheck.Editor.asmdef b/Aspid.FastTools/Assets/PlayerBuildCheck~/Editor/Aspid.FastTools.PlayerBuildCheck.Editor.asmdef new file mode 100644 index 00000000..bcfa9135 --- /dev/null +++ b/Aspid.FastTools/Assets/PlayerBuildCheck~/Editor/Aspid.FastTools.PlayerBuildCheck.Editor.asmdef @@ -0,0 +1,20 @@ +{ + "name": "Aspid.FastTools.PlayerBuildCheck.Editor", + "rootNamespace": "Aspid.FastTools.PlayerBuildCheck", + "references": [ + "Aspid.FastTools", + "Aspid.FastTools.PlayerBuildCheck", + "Aspid.FastTools.Samples.Types" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": false, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/Aspid.FastTools/Assets/PlayerBuildCheck~/Editor/Aspid.FastTools.PlayerBuildCheck.Editor.asmdef.meta b/Aspid.FastTools/Assets/PlayerBuildCheck~/Editor/Aspid.FastTools.PlayerBuildCheck.Editor.asmdef.meta new file mode 100644 index 00000000..bbd37663 --- /dev/null +++ b/Aspid.FastTools/Assets/PlayerBuildCheck~/Editor/Aspid.FastTools.PlayerBuildCheck.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 872a3e65eb3f44fc96d3d04a7a20a844 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Assets/PlayerBuildCheck~/Editor/PlayerBuilds.cs b/Aspid.FastTools/Assets/PlayerBuildCheck~/Editor/PlayerBuilds.cs new file mode 100644 index 00000000..9d9e07b7 --- /dev/null +++ b/Aspid.FastTools/Assets/PlayerBuildCheck~/Editor/PlayerBuilds.cs @@ -0,0 +1,167 @@ +using System; +using System.IO; +using System.Linq; +using UnityEditor; +using UnityEngine; +using UnityEditor.Build; +using UnityEditor.Build.Reporting; +using UnityEditor.SceneManagement; +using System.Collections.Generic; +using Aspid.FastTools.Samples.Types.Spawning; + +// ReSharper disable once CheckNamespace +namespace Aspid.FastTools.PlayerBuildCheck +{ + // Batch-mode entry point of scripts/player-builds.sh: + // -executeMethod Aspid.FastTools.PlayerBuildCheck.PlayerBuilds.Build + // -playerBuildTargets mac-mono,mac-il2cpp-minimal,mac-il2cpp-high,ios-sim -playerBuildOutput + // Builds every target without BuildOptions.Development, writes /builds.tsv and exits with 1 when one fails. + public static class PlayerBuilds + { + private const string ProductName = "PlayerBuildCheck"; + private const string BundleId = "tech.aspid.fasttools.playerbuildcheck"; + private const string ScenePath = "Assets/PlayerBuildCheck/PlayerBuildCheck.unity"; + + private static readonly Target[] _targets = + { + new("mac-mono", BuildTarget.StandaloneOSX, ScriptingImplementation.Mono2x, ManagedStrippingLevel.Disabled), + new("mac-il2cpp-minimal", BuildTarget.StandaloneOSX, ScriptingImplementation.IL2CPP, ManagedStrippingLevel.Minimal), + new("mac-il2cpp-high", BuildTarget.StandaloneOSX, ScriptingImplementation.IL2CPP, ManagedStrippingLevel.High), + new("ios-sim", BuildTarget.iOS, ScriptingImplementation.IL2CPP, ManagedStrippingLevel.High), + }; + + public static void Build() + { + try + { + var ids = Argument("-playerBuildTargets").Split(',', StringSplitOptions.RemoveEmptyEntries); + var output = Argument("-playerBuildOutput"); + Directory.CreateDirectory(output); + + CreateScene(); + + var lines = new List(); + var hasFailed = false; + + foreach (var id in ids) + { + var target = _targets.FirstOrDefault(target => target.Id == id) + ?? throw new ArgumentException($"Unknown target {id}"); + + var summary = BuildOne(target, output); + hasFailed |= summary.result != BuildResult.Succeeded; + lines.Add($"{id}\t{summary.result}\t{summary.totalErrors}\t{summary.totalWarnings}\t{summary.options}"); + File.WriteAllLines(Path.Combine(output, "builds.tsv"), lines); + } + + EditorApplication.Exit(hasFailed ? 1 : 0); + } + catch (Exception exception) + { + Debug.LogException(exception); + EditorApplication.Exit(2); + } + } + + private static BuildSummary BuildOne(Target target, string output) + { + var namedTarget = NamedBuildTarget.FromBuildTargetGroup(BuildPipeline.GetBuildTargetGroup(target.BuildTarget)); + PlayerSettings.productName = ProductName; + PlayerSettings.SetApplicationIdentifier(namedTarget, BundleId); + PlayerSettings.SetScriptingBackend(namedTarget, target.Backend); + PlayerSettings.SetManagedStrippingLevel(namedTarget, target.Stripping); + + if (target.BuildTarget == BuildTarget.StandaloneOSX) + UnityEditor.OSXStandalone.UserBuildSettings.architecture = OSArchitecture.ARM64; + + if (target.BuildTarget == BuildTarget.iOS) + { + PlayerSettings.iOS.sdkVersion = iOSSdkVersion.SimulatorSDK; + PlayerSettings.iOS.simulatorSdkArchitecture = AppleMobileArchitectureSimulator.ARM64; + // Xcode 27 refuses the default deployment target of Unity 6000.0 (13.0) for the simulator. + PlayerSettings.iOS.targetOSVersionString = "15.0"; + } + + var path = target.BuildTarget == BuildTarget.iOS + ? Path.Combine(output, target.Id) + : Path.Combine(output, target.Id, $"{ProductName}.app"); + + var report = BuildPipeline.BuildPlayer(new BuildPlayerOptions + { + scenes = new[] { ScenePath }, + locationPathName = path, + target = target.BuildTarget, + options = BuildOptions.None, + }); + + var summary = report.summary; + Debug.Log($"PLAYERBUILD {target.Id} result={summary.result} errors={summary.totalErrors} " + + $"warnings={summary.totalWarnings} options={summary.options} time={summary.totalTime} path={path}"); + + foreach (var message in report.steps.SelectMany(step => step.messages)) + { + if (message.type is LogType.Error or LogType.Exception or LogType.Warning) + Debug.Log($"PLAYERBUILD {target.Id} {message.type}: {message.content}"); + } + + return summary; + } + + private static void CreateScene() + { + var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single); + var probe = new GameObject(ProductName).AddComponent(); + + // The names are written as strings: a typeof in the player code would keep the type from stripping. + var serialized = new SerializedObject(probe); + Set(serialized, field: "_preserved", name: typeof(PlayerBuildProbePreserved).AssemblyQualifiedName); + Set(serialized, field: "_engine", name: typeof(Transform).AssemblyQualifiedName); + Set(serialized, field: "_generic", name: typeof(List).AssemblyQualifiedName); + Set(serialized, field: "_plain", name: typeof(PlayerBuildProbeTarget).AssemblyQualifiedName); + Set(serialized, field: "_strippedEngine", name: typeof(BoxCollider).AssemblyQualifiedName); + Set(serialized, field: "_samplePattern", name: typeof(LinePattern).AssemblyQualifiedName); + serialized.ApplyModifiedPropertiesWithoutUndo(); + + Directory.CreateDirectory(Path.GetDirectoryName(ScenePath)!); + EditorSceneManager.SaveScene(scene, ScenePath); + AssetDatabase.SaveAssets(); + } + + private static void Set(SerializedObject serialized, string field, string name) + { + var property = serialized.FindProperty($"{field}._assemblyQualifiedName") + ?? throw new InvalidOperationException($"{field}._assemblyQualifiedName not found"); + + property.stringValue = name; + } + + private static string Argument(string name) + { + var args = Environment.GetCommandLineArgs(); + var index = Array.IndexOf(args, name); + + return index >= 0 && index + 1 < args.Length + ? args[index + 1] + : throw new ArgumentException($"{name} is required"); + } + + private sealed class Target + { + public Target(string id, BuildTarget buildTarget, ScriptingImplementation backend, ManagedStrippingLevel stripping) + { + Id = id; + BuildTarget = buildTarget; + Backend = backend; + Stripping = stripping; + } + + public string Id { get; } + + public BuildTarget BuildTarget { get; } + + public ScriptingImplementation Backend { get; } + + public ManagedStrippingLevel Stripping { get; } + } + } +} diff --git a/Aspid.FastTools/Assets/PlayerBuildCheck~/Editor/PlayerBuilds.cs.meta b/Aspid.FastTools/Assets/PlayerBuildCheck~/Editor/PlayerBuilds.cs.meta new file mode 100644 index 00000000..c071b484 --- /dev/null +++ b/Aspid.FastTools/Assets/PlayerBuildCheck~/Editor/PlayerBuilds.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 17d194327c33495eaf097c1c00543178 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Assets/PlayerBuildCheck~/PlayerBuildProbe.cs b/Aspid.FastTools/Assets/PlayerBuildCheck~/PlayerBuildProbe.cs new file mode 100644 index 00000000..e5403c4e --- /dev/null +++ b/Aspid.FastTools/Assets/PlayerBuildCheck~/PlayerBuildProbe.cs @@ -0,0 +1,95 @@ +using System; +using UnityEngine; +using Aspid.FastTools.Types; +using Aspid.FastTools.Samples.Types.Spawning; + +// ReSharper disable once CheckNamespace +namespace Aspid.FastTools.PlayerBuildCheck +{ + // Resolves the stored type names in a player and logs one AOTCHECK line per field, then quits. + // Required fields resolve on every target. Reference fields hold names that stripping may remove: they are only logged. + public sealed class PlayerBuildProbe : MonoBehaviour + { + [SerializeField] private SerializableType _preserved; + [SerializeField] private SerializableType _engine; + [SerializeField] private SerializableType _generic; + + [SerializeField] private SerializableType _plain; + [SerializeField] private SerializableType _strippedEngine; + [SerializeField] private SerializableType _samplePattern; + + private static string Backend => +#if ENABLE_IL2CPP + "il2cpp"; +#else + "mono"; +#endif + + private static string Profiler => +#if ENABLE_PROFILER + "on"; +#else + "off"; +#endif + + private static string Development => +#if DEVELOPMENT_BUILD + "on"; +#else + "off"; +#endif + + private void Start() + { + Application.SetStackTraceLogType(LogType.Log, StackTraceLogType.None); + + using (this.Marker()) + { + Report(kind: "required", field: "preserved", value: _preserved); + Report(kind: "required", field: "engine", value: _engine); + Report(kind: "required", field: "generic", value: _generic); + Report(kind: "reference", field: "plain", value: _plain); + Report(kind: "reference", field: "strippedEngine", value: _strippedEngine); + Report(kind: "reference", field: "samplePattern", value: _samplePattern); + } + + using (this.Marker().WithName("Instantiate")) + ReportInstance(); + + Debug.Log($"AOTCHECK profiler={Profiler} development={Development} debug={Debug.isDebugBuild} " + + $"backend={Backend} platform={Application.platform} unity={Application.unityVersion}"); + Debug.Log("AOTCHECK DONE"); + Application.Quit(); + } + + private void ReportInstance() + { + if (_samplePattern.Type is not { } type) + { + Debug.Log("AOTCHECK reference samplePatternInstance MISSING type not resolved"); + return; + } + + try + { + var pattern = (ISpawnPattern)Activator.CreateInstance(type); + var position = pattern.GetPosition(index: 1, count: 4, radius: 2f); + Debug.Log($"AOTCHECK reference samplePatternInstance ok {type.FullName} {position}"); + } + catch (Exception exception) + { + Debug.Log($"AOTCHECK reference samplePatternInstance MISSING {exception.GetType().Name}: {exception.Message}"); + } + } + + private static void Report(string kind, string field, SerializableTypeBase value) + { + var state = value?.Type is null + ? "MISSING" + : "ok"; + + Debug.Log($"AOTCHECK {kind} {field} {state} name={value?.AssemblyQualifiedName} " + + $"type={value?.Type?.FullName} IsMissing={value?.IsMissing}"); + } + } +} diff --git a/Aspid.FastTools/Assets/PlayerBuildCheck~/PlayerBuildProbe.cs.meta b/Aspid.FastTools/Assets/PlayerBuildCheck~/PlayerBuildProbe.cs.meta new file mode 100644 index 00000000..7daa2cc8 --- /dev/null +++ b/Aspid.FastTools/Assets/PlayerBuildCheck~/PlayerBuildProbe.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d4c964610a4d45dc917da6b8660d706f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Assets/PlayerBuildCheck~/PlayerBuildProbePreserved.cs b/Aspid.FastTools/Assets/PlayerBuildCheck~/PlayerBuildProbePreserved.cs new file mode 100644 index 00000000..d30e4baf --- /dev/null +++ b/Aspid.FastTools/Assets/PlayerBuildCheck~/PlayerBuildProbePreserved.cs @@ -0,0 +1,8 @@ +using UnityEngine.Scripting; + +// ReSharper disable once CheckNamespace +namespace Aspid.FastTools.PlayerBuildCheck +{ + [Preserve] + public sealed class PlayerBuildProbePreserved { } +} diff --git a/Aspid.FastTools/Assets/PlayerBuildCheck~/PlayerBuildProbePreserved.cs.meta b/Aspid.FastTools/Assets/PlayerBuildCheck~/PlayerBuildProbePreserved.cs.meta new file mode 100644 index 00000000..2368613d --- /dev/null +++ b/Aspid.FastTools/Assets/PlayerBuildCheck~/PlayerBuildProbePreserved.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a5e4c458bcbe492ba87e5d25bf7ab68f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Assets/PlayerBuildCheck~/PlayerBuildProbeTarget.cs b/Aspid.FastTools/Assets/PlayerBuildCheck~/PlayerBuildProbeTarget.cs new file mode 100644 index 00000000..41657afe --- /dev/null +++ b/Aspid.FastTools/Assets/PlayerBuildCheck~/PlayerBuildProbeTarget.cs @@ -0,0 +1,6 @@ +// ReSharper disable once CheckNamespace +namespace Aspid.FastTools.PlayerBuildCheck +{ + // Referenced only by its stored name: from Managed Stripping Level Medium up the linker removes it. + public sealed class PlayerBuildProbeTarget { } +} diff --git a/Aspid.FastTools/Assets/PlayerBuildCheck~/PlayerBuildProbeTarget.cs.meta b/Aspid.FastTools/Assets/PlayerBuildCheck~/PlayerBuildProbeTarget.cs.meta new file mode 100644 index 00000000..123bbcc3 --- /dev/null +++ b/Aspid.FastTools/Assets/PlayerBuildCheck~/PlayerBuildProbeTarget.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 980dda094ffe4a36936b2b88a045654e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From 36697b5eb9c617590a28561ff437b066472eaa2e Mon Sep 17 00:00:00 2001 From: Vladislav Panin Date: Sat, 24 Oct 2026 18:21:36 +0300 Subject: [PATCH 2/4] chore(scripts): add one-command player builds - scripts/player-builds.sh builds Mac Mono, Mac IL2CPP (Minimal, High) and the iOS Simulator, runs the players and prints the AOTCHECK table. - The asp-ft-player-builds skill runs it and reads the table; asp-ft-release and AGENTS.md point to it. - PlayerBuilds appends to builds.tsv, so the Mac and iOS Editor runs keep both results. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01T2kWtnmRdgnk2YsZd4MdvY --- .claude/skills/asp-ft-player-builds/SKILL.md | 67 +++++++ .claude/skills/asp-ft-release/SKILL.md | 5 +- Aspid.FastTools/AGENTS.md | 2 + .../PlayerBuildCheck~/Editor/PlayerBuilds.cs | 11 +- scripts/player-builds.sh | 176 ++++++++++++++++++ 5 files changed, 255 insertions(+), 6 deletions(-) create mode 100644 .claude/skills/asp-ft-player-builds/SKILL.md create mode 100755 scripts/player-builds.sh diff --git a/.claude/skills/asp-ft-player-builds/SKILL.md b/.claude/skills/asp-ft-player-builds/SKILL.md new file mode 100644 index 00000000..bdf445f0 --- /dev/null +++ b/.claude/skills/asp-ft-player-builds/SKILL.md @@ -0,0 +1,67 @@ +--- +name: asp-ft-player-builds +description: Build and run release players of Aspid.FastTools on macOS in one command - Mac Mono, Mac IL2CPP (Managed Stripping Level Minimal and High) and the iOS Simulator - and check that SerializableType resolves its stored names and that ENABLE_PROFILER is off. Covers the "player builds" items of the release checklist. - `/asp-ft-player-builds`, `/asp-ft-player-builds 6000.3.26f1`. +argument-hint: "[unity-version]" +disable-model-invocation: true +--- + +# Player builds + +The argument is the Unity version. No argument → the minimum: the first version of `UNITY_VERSIONS` in +`.github/workflows/ci.yml`. + +## When + +- The manual QA of a **stable** release, before the merge of the release PR: the items "player builds on Mono and + IL2CPP", "non-development build", "IL2CPP stripping and `SerializableType`" and "one standalone and one AOT target" + of `.github/ISSUE_TEMPLATE/release_checklist.yml`. +- A change to `SerializableType`, the type lookup, the profiler marker generator or `ENABLE_PROFILER` code. + +## Run + +Run it on the branch under test: for a release, a worktree of `chore/release-`. The script builds the +package of its own checkout. + +```bash +scripts/player-builds.sh /player-builds [unity-version] +``` + +- `/player-builds` must not exist or be empty. It gets `Project/`, `Builds/` and `Logs/`. +- Run it in the background: all four targets take about 3–4 minutes. +- `--targets mac-mono,ios-sim` limits the run to the listed targets. +- It needs macOS, the Unity Editor of the version, its modules `mac-il2cpp` and `ios`, and Xcode for `ios-sim`. A + missing module skips its target and prints the Unity Hub command that installs it. Ask the user before you run an + install: it downloads gigabytes. +- The simulator runs without its window. The script shuts it down after the run when it booted it. + +## Read the table + +``` +TARGET BUILD ERR/WARN AOTCHECK PROFILER REFERENCE MISSING +mac-mono Succeeded 0/0 3/3 off dev=off none +``` + +- `BUILD`: the `BuildReport` result. `xcodebuild-failed`: Unity made the Xcode project, Xcode did not build it. +- `ERR/WARN`: the errors and warnings of the `BuildReport`. +- `AOTCHECK`: the required fields that resolved in the player, out of all. Required are a `[Preserve]` class, + `Transform` and `List`: each must resolve on every target. `no-run`: the player did not print `AOTCHECK DONE`. +- `PROFILER`: `ENABLE_PROFILER` and `DEVELOPMENT_BUILD` in the player. Both must be `off`. +- `REFERENCE MISSING`: reference fields that did not resolve. They are informational and do not fail the run: + - `plain`, a class referenced only by its stored name, and `samplePattern` with `samplePatternInstance`, + `LinePattern` of the Types sample: the linker removes them from Managed Stripping Level Medium up. The XML docs of + `SerializableTypeBase` say so. + - `strippedEngine`, `BoxCollider`: Strip Engine Code removes the physics module, which the check scene does not use. + - A reference field that turns `MISSING` on Mono or Minimal is a regression. + +The exit code is 1 when a target is skipped or fails. The logs are in `Logs/`: `unity-*.log` for the builds, +`run-*.log` for the players, `xcodebuild-ios-sim.log`. The iOS Simulator player is always a development engine build, +so its log says "Build type 'Development'" and `debug=True`: the `PROFILER` column still checks the defines. + +## Report + +Write the report in Russian. Keep it short: + +1. The first line: the verdict, the Unity version and the commit. +2. The table as the script printed it. +3. Each failure: the target and the first error lines from its log. +4. The release checklist items that the run closes. diff --git a/.claude/skills/asp-ft-release/SKILL.md b/.claude/skills/asp-ft-release/SKILL.md index a3ae0c8d..c25ec5e0 100644 --- a/.claude/skills/asp-ft-release/SKILL.md +++ b/.claude/skills/asp-ft-release/SKILL.md @@ -203,8 +203,9 @@ Put these items into the Phase 1 report. Do not do them without a request. without Unity says that the reference still needs regenerating. Ask if such a PR is merged since the last release. - "The channel changed" from `set-version.sh` → re-record the README install walk-through in a separate PR (`Website/media/readme-previews/README.md`). -- **Stable:** the manual QA of `.github/ISSUE_TEMPLATE/release_checklist.yml`: player builds, and the Asset Store - upload as in "Asset Store upload". It comes before the merge of the release PR. +- **Stable:** the manual QA of `.github/ISSUE_TEMPLATE/release_checklist.yml`: player builds with the + `asp-ft-player-builds` skill, and the Asset Store upload as in "Asset Store upload". It comes before the merge of + the release PR. ## Report format diff --git a/Aspid.FastTools/AGENTS.md b/Aspid.FastTools/AGENTS.md index 422f5d7f..da7c75d2 100644 --- a/Aspid.FastTools/AGENTS.md +++ b/Aspid.FastTools/AGENTS.md @@ -48,5 +48,7 @@ From the repository root: - Compile in the dev project with its Editor open: `unity command script recompile`, then `unity command console get`. - EditMode tests on any Unity version: `scripts/make-unity-test-project.sh [unity-version]`; its header shows the `-runTests` command. +- Release players on macOS (Mono, IL2CPP, iOS Simulator) with the `SerializableType` and `ENABLE_PROFILER` check: + `scripts/player-builds.sh [unity-version]`, read by the `asp-ft-player-builds` skill. - YAML engine: `dotnet test DotnetTests/Yaml`. - Type resolver: `dotnet test DotnetTests/Types`. diff --git a/Aspid.FastTools/Assets/PlayerBuildCheck~/Editor/PlayerBuilds.cs b/Aspid.FastTools/Assets/PlayerBuildCheck~/Editor/PlayerBuilds.cs index 9d9e07b7..2456753d 100644 --- a/Aspid.FastTools/Assets/PlayerBuildCheck~/Editor/PlayerBuilds.cs +++ b/Aspid.FastTools/Assets/PlayerBuildCheck~/Editor/PlayerBuilds.cs @@ -15,7 +15,8 @@ namespace Aspid.FastTools.PlayerBuildCheck // Batch-mode entry point of scripts/player-builds.sh: // -executeMethod Aspid.FastTools.PlayerBuildCheck.PlayerBuilds.Build // -playerBuildTargets mac-mono,mac-il2cpp-minimal,mac-il2cpp-high,ios-sim -playerBuildOutput - // Builds every target without BuildOptions.Development, writes /builds.tsv and exits with 1 when one fails. + // Builds every target without BuildOptions.Development, appends a line per target to /builds.tsv and exits + // with 1 when one fails. public static class PlayerBuilds { private const string ProductName = "PlayerBuildCheck"; @@ -40,7 +41,6 @@ public static void Build() CreateScene(); - var lines = new List(); var hasFailed = false; foreach (var id in ids) @@ -50,8 +50,11 @@ public static void Build() var summary = BuildOne(target, output); hasFailed |= summary.result != BuildResult.Succeeded; - lines.Add($"{id}\t{summary.result}\t{summary.totalErrors}\t{summary.totalWarnings}\t{summary.options}"); - File.WriteAllLines(Path.Combine(output, "builds.tsv"), lines); + // Appended: the script runs the Editor once per build target group into one output folder. + File.AppendAllLines(Path.Combine(output, "builds.tsv"), new[] + { + $"{id}\t{summary.result}\t{summary.totalErrors}\t{summary.totalWarnings}\t{summary.options}", + }); } EditorApplication.Exit(hasFailed ? 1 : 0); diff --git a/scripts/player-builds.sh b/scripts/player-builds.sh new file mode 100755 index 00000000..d9f7dd41 --- /dev/null +++ b/scripts/player-builds.sh @@ -0,0 +1,176 @@ +#!/usr/bin/env bash +# Builds release players of the package on macOS and runs them: the manual "player builds" QA of +# .github/ISSUE_TEMPLATE/release_checklist.yml in one command. The asp-ft-player-builds skill reads the result. +# +# Usage: scripts/player-builds.sh [--targets ] [unity-version] +# --targets comma-separated; default: mac-mono,mac-il2cpp-minimal,mac-il2cpp-high,ios-sim +# mac-mono StandaloneOSX, Mono, no stripping +# mac-il2cpp-minimal StandaloneOSX, IL2CPP, Managed Stripping Level Minimal +# mac-il2cpp-high StandaloneOSX, IL2CPP, Managed Stripping Level High +# ios-sim iOS Simulator (arm64), IL2CPP, High: Xcode project, xcodebuild, simctl run +# project-dir must not exist or be empty; it gets the test project, Builds/ and Logs/. +# unity-version default: the minimum, the first version of UNITY_VERSIONS in .github/workflows/ci.yml. +# +# Every player is built without BuildOptions.Development and runs Aspid.FastTools/Assets/PlayerBuildCheck~: its +# AOTCHECK lines tell whether SerializableType resolves the stored names, and whether ENABLE_PROFILER is off. +# A target whose Unity module is missing is skipped with the install command. Exits with 1 when a target fails. +set -euo pipefail + +ALL_TARGETS="mac-mono,mac-il2cpp-minimal,mac-il2cpp-high,ios-sim" +TARGETS="$ALL_TARGETS" +if [ "${1:-}" = "--targets" ]; then + TARGETS="${2:?--targets needs a list}" + shift 2 +fi +if [ $# -lt 1 ] || [ $# -gt 2 ]; then + echo "usage: $0 [--targets ] [unity-version]" >&2 + exit 2 +fi + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +VERSION="${2:-$(sed -n 's/^ *UNITY_VERSIONS: \([^ ]*\).*/\1/p' "$ROOT/.github/workflows/ci.yml")}" +EDITOR_DIR="/Applications/Unity/Hub/Editor/$VERSION" +UNITY="$EDITOR_DIR/Unity.app/Contents/MacOS/Unity" +HUB="/Applications/Unity Hub.app/Contents/MacOS/Unity Hub" +PRODUCT="PlayerBuildCheck" +BUNDLE_ID="tech.aspid.fasttools.playerbuildcheck" +PLAYER_TIMEOUT=120 + +if [ ! -x "$UNITY" ]; then + echo "error: Unity $VERSION is not installed: \"$HUB\" -- --headless install --version $VERSION" >&2 + exit 1 +fi + +if [ -e "$1" ] && [ -n "$(ls -A "$1")" ]; then + echo "error: $1 exists and is not empty" >&2 + exit 1 +fi +mkdir -p "$1" +# A physical path keeps the relative file: reference of the test project valid where /var or /tmp is a symlink. +PROJECT="$(cd "$1" && pwd -P)" +"$ROOT/scripts/make-unity-test-project.sh" "$PROJECT/Project" "$VERSION" >/dev/null +cp -R "$ROOT/Aspid.FastTools/Assets/PlayerBuildCheck~" "$PROJECT/Project/Assets/PlayerBuildCheck" +BUILDS="$PROJECT/Builds" +LOGS="$PROJECT/Logs" +mkdir -p "$BUILDS" "$LOGS" + +# The module a target needs, or nothing when it is installed. +missing_module() { + case "$1" in + mac-mono) ;; + mac-il2cpp-*) + ls -d "$EDITOR_DIR/Unity.app/Contents/PlaybackEngines/MacStandaloneSupport/Variations/"*il2cpp* >/dev/null 2>&1 || + echo "mac-il2cpp" ;; + ios-sim) + [ -d "$EDITOR_DIR/PlaybackEngines/iOSSupport" ] || [ -d "$EDITOR_DIR/Unity.app/Contents/PlaybackEngines/iOSSupport" ] || + echo "ios" ;; + *) + echo "error: unknown target $1" >&2 + exit 2 ;; + esac +} + +declare -a MAC=() IOS=() SKIPPED=() +IFS=',' read -r -a requested <<< "$TARGETS" +for target in "${requested[@]}"; do + module="$(missing_module "$target")" + if [ -n "$module" ]; then + SKIPPED+=("$target") + echo "skip $target: needs the Unity module $module: \"$HUB\" -- --headless install-modules --version $VERSION --module $module" >&2 + elif [ "$target" = "ios-sim" ]; then + if xcrun simctl help >/dev/null 2>&1; then IOS+=("$target"); else SKIPPED+=("$target"); echo "skip $target: needs Xcode" >&2; fi + else + MAC+=("$target") + fi +done + +# One Editor run per build target group: the Mac targets, then iOS. +build() { + local build_target="$1" log="$2" + shift 2 + local list + list="$(IFS=','; echo "$*")" + "$UNITY" -batchmode -nographics -projectPath "$PROJECT/Project" -buildTarget "$build_target" \ + -executeMethod Aspid.FastTools.PlayerBuildCheck.PlayerBuilds.Build \ + -playerBuildTargets "$list" -playerBuildOutput "$BUILDS" -logFile "$LOGS/$log" || true +} + +run_with_timeout() { + perl -e 'alarm shift; exec @ARGV' "$PLAYER_TIMEOUT" "$@" +} + +# ${a[@]+"${a[@]}"}: bash 3.2, the macOS bash, treats an empty array as unset under set -u. +[ ${#MAC[@]} -gt 0 ] && build OSXUniversal unity-mac.log "${MAC[@]}" + +for target in ${MAC[@]+"${MAC[@]}"}; do + app="$BUILDS/$target/$PRODUCT.app/Contents/MacOS/$PRODUCT" + [ -x "$app" ] && { run_with_timeout "$app" -batchmode -nographics -logFile "$LOGS/run-$target.log" >/dev/null 2>&1 || true; } +done + +if [ ${#IOS[@]} -gt 0 ]; then + build iOS unity-ios.log "${IOS[@]}" + if [ -d "$BUILDS/ios-sim/Unity-iPhone.xcodeproj" ]; then + xcodebuild -project "$BUILDS/ios-sim/Unity-iPhone.xcodeproj" -scheme Unity-iPhone -sdk iphonesimulator \ + -configuration Release -derivedDataPath "$BUILDS/ios-sim-DerivedData" CODE_SIGNING_ALLOWED=NO ARCHS=arm64 \ + ONLY_ACTIVE_ARCH=YES build > "$LOGS/xcodebuild-ios-sim.log" 2>&1 || true + fi + app="$BUILDS/ios-sim-DerivedData/Build/Products/Release-iphonesimulator/$PRODUCT.app" + if [ -d "$app" ]; then + # The first available iPhone; a booted one first. The simulator runs without its window. + read -r device state < <(xcrun simctl list devices available --json | python3 -c ' +import json, sys +devices = [d for runtime in json.load(sys.stdin)["devices"].values() for d in runtime if d["name"].startswith("iPhone")] +devices.sort(key=lambda d: d["state"] != "Booted") +print(devices[0]["udid"], devices[0]["state"]) if devices else print("", "")') + if [ -n "$device" ]; then + [ "$state" = "Booted" ] || xcrun simctl boot "$device" + xcrun simctl bootstatus "$device" -b >/dev/null 2>&1 || true + xcrun simctl install "$device" "$app" + run_with_timeout xcrun simctl launch --console-pty --terminate-running-process "$device" "$BUNDLE_ID" \ + > "$LOGS/run-ios-sim.log" 2>&1 || true + [ "$state" = "Booted" ] || xcrun simctl shutdown "$device" || true + else + echo "ios-sim: no available iPhone simulator" >&2 + fi + fi +fi + +# The table: one row per requested target. +status=0 +printf '%-20s %-10s %-9s %-10s %-9s %s\n' TARGET BUILD ERR/WARN AOTCHECK PROFILER "REFERENCE MISSING" +for target in "${requested[@]}"; do + if [[ " ${SKIPPED[*]+"${SKIPPED[*]}"} " == *" $target "* ]]; then + printf '%-20s %s\n' "$target" "skipped: missing module or Xcode" + status=1 + continue + fi + + row="$(awk -F '\t' -v target="$target" '$1 == target' "$BUILDS/builds.tsv" 2>/dev/null || true)" + result="$(cut -f2 <<< "$row")" + counts="$(cut -f3 <<< "$row")/$(cut -f4 <<< "$row")" + [ -n "$row" ] || { result="none"; counts="-"; } + + log="$LOGS/run-$target.log" + if [ "$target" = "ios-sim" ] && ! grep -q '\*\* BUILD SUCCEEDED \*\*' "$LOGS/xcodebuild-ios-sim.log" 2>/dev/null; then + result="xcodebuild-failed" + fi + + if grep -q 'AOTCHECK DONE' "$log" 2>/dev/null; then + required="$(grep -c 'AOTCHECK required .* ok ' "$log" || true)" + required_total="$(grep -c 'AOTCHECK required ' "$log" || true)" + aotcheck="$required/$required_total" + profiler="$(sed -n 's/.*AOTCHECK profiler=\([a-z]*\) development=\([a-z]*\).*/\1 dev=\2/p' "$log" | head -n 1)" + reference="$(sed -n 's/.*AOTCHECK reference \([A-Za-z]*\) MISSING.*/\1/p' "$log" | sort -u | paste -sd, -)" + else + required=0; required_total=1; aotcheck="no-run"; profiler="-"; reference="-" + fi + + printf '%-20s %-10s %-9s %-10s %-9s %s\n' "$target" "$result" "$counts" "$aotcheck" "$profiler" "${reference:-none}" + + if [ "$result" != "Succeeded" ] || [ "$required" != "$required_total" ] || [ "$profiler" != "off dev=off" ]; then + status=1 + fi +done + +echo "logs: $LOGS" +exit $status From cfa4955317bb4cb27ccbde14acb1e89003010ff8 Mon Sep 17 00:00:00 2001 From: Vladislav Panin Date: Sat, 24 Oct 2026 18:21:49 +0300 Subject: [PATCH 3/4] docs(skills): name the reference fields that stay missing on Minimal Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01T2kWtnmRdgnk2YsZd4MdvY --- .claude/skills/asp-ft-player-builds/SKILL.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.claude/skills/asp-ft-player-builds/SKILL.md b/.claude/skills/asp-ft-player-builds/SKILL.md index bdf445f0..70542926 100644 --- a/.claude/skills/asp-ft-player-builds/SKILL.md +++ b/.claude/skills/asp-ft-player-builds/SKILL.md @@ -50,8 +50,9 @@ mac-mono Succeeded 0/0 3/3 off dev=off none - `plain`, a class referenced only by its stored name, and `samplePattern` with `samplePatternInstance`, `LinePattern` of the Types sample: the linker removes them from Managed Stripping Level Medium up. The XML docs of `SerializableTypeBase` say so. - - `strippedEngine`, `BoxCollider`: Strip Engine Code removes the physics module, which the check scene does not use. - - A reference field that turns `MISSING` on Mono or Minimal is a regression. + - `strippedEngine`, `BoxCollider`: on every IL2CPP target Strip Engine Code removes the physics module, which the + check scene does not use. + - `plain` or `samplePattern` that turns `MISSING` on `mac-mono` or `mac-il2cpp-minimal` is a regression. The exit code is 1 when a target is skipped or fails. The logs are in `Logs/`: `unity-*.log` for the builds, `run-*.log` for the players, `xcodebuild-ios-sim.log`. The iOS Simulator player is always a development engine build, From 1794d03f875135a99cb1ebcfe988fec9663563c6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 15:25:26 +0000 Subject: [PATCH 4/4] chore(release): validate player-builds targets and keep the table on simulator errors - Check every --targets id against the known list before the project is made. - A failed simctl boot or install leaves ios-sim at no-run instead of ending the run under set -e. - Stop early on a non-arm64 Mac and name Apple Silicon in the header and the skill. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01T2kWtnmRdgnk2YsZd4MdvY --- .claude/skills/asp-ft-player-builds/SKILL.md | 6 +-- scripts/player-builds.sh | 42 +++++++++++++++----- 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/.claude/skills/asp-ft-player-builds/SKILL.md b/.claude/skills/asp-ft-player-builds/SKILL.md index 70542926..009fedbe 100644 --- a/.claude/skills/asp-ft-player-builds/SKILL.md +++ b/.claude/skills/asp-ft-player-builds/SKILL.md @@ -29,9 +29,9 @@ scripts/player-builds.sh /player-builds [unity-version] - `/player-builds` must not exist or be empty. It gets `Project/`, `Builds/` and `Logs/`. - Run it in the background: all four targets take about 3–4 minutes. - `--targets mac-mono,ios-sim` limits the run to the listed targets. -- It needs macOS, the Unity Editor of the version, its modules `mac-il2cpp` and `ios`, and Xcode for `ios-sim`. A - missing module skips its target and prints the Unity Hub command that installs it. Ask the user before you run an - install: it downloads gigabytes. +- It needs an Apple Silicon Mac (the players are arm64), the Unity Editor of the version, its modules `mac-il2cpp` + and `ios`, and Xcode for `ios-sim`. A missing module skips its target and prints the Unity Hub command that installs + it. Ask the user before you run an install: it downloads gigabytes. - The simulator runs without its window. The script shuts it down after the run when it booted it. ## Read the table diff --git a/scripts/player-builds.sh b/scripts/player-builds.sh index d9f7dd41..40c86524 100755 --- a/scripts/player-builds.sh +++ b/scripts/player-builds.sh @@ -11,6 +11,8 @@ # project-dir must not exist or be empty; it gets the test project, Builds/ and Logs/. # unity-version default: the minimum, the first version of UNITY_VERSIONS in .github/workflows/ci.yml. # +# Needs an Apple Silicon Mac: the Mac players and the simulator player are arm64. +# # Every player is built without BuildOptions.Development and runs Aspid.FastTools/Assets/PlayerBuildCheck~: its # AOTCHECK lines tell whether SerializableType resolves the stored names, and whether ENABLE_PROFILER is off. # A target whose Unity module is missing is skipped with the install command. Exits with 1 when a target fails. @@ -27,6 +29,21 @@ if [ $# -lt 1 ] || [ $# -gt 2 ]; then exit 2 fi +IFS=',' read -r -a requested <<< "$TARGETS" +for target in "${requested[@]}"; do + case ",$ALL_TARGETS," in + *",$target,"*) ;; + *) + echo "error: unknown target $target; known: $ALL_TARGETS" >&2 + exit 2 ;; + esac +done + +if [ "$(uname -m)" != "arm64" ]; then + echo "error: needs an Apple Silicon Mac: the players are built for arm64" >&2 + exit 1 +fi + ROOT="$(cd "$(dirname "$0")/.." && pwd)" VERSION="${2:-$(sed -n 's/^ *UNITY_VERSIONS: \([^ ]*\).*/\1/p' "$ROOT/.github/workflows/ci.yml")}" EDITOR_DIR="/Applications/Unity/Hub/Editor/$VERSION" @@ -58,20 +75,16 @@ mkdir -p "$BUILDS" "$LOGS" missing_module() { case "$1" in mac-mono) ;; - mac-il2cpp-*) + mac-il2cpp-minimal | mac-il2cpp-high) ls -d "$EDITOR_DIR/Unity.app/Contents/PlaybackEngines/MacStandaloneSupport/Variations/"*il2cpp* >/dev/null 2>&1 || echo "mac-il2cpp" ;; ios-sim) [ -d "$EDITOR_DIR/PlaybackEngines/iOSSupport" ] || [ -d "$EDITOR_DIR/Unity.app/Contents/PlaybackEngines/iOSSupport" ] || echo "ios" ;; - *) - echo "error: unknown target $1" >&2 - exit 2 ;; esac } declare -a MAC=() IOS=() SKIPPED=() -IFS=',' read -r -a requested <<< "$TARGETS" for target in "${requested[@]}"; do module="$(missing_module "$target")" if [ -n "$module" ]; then @@ -123,12 +136,19 @@ devices = [d for runtime in json.load(sys.stdin)["devices"].values() for d in ru devices.sort(key=lambda d: d["state"] != "Booted") print(devices[0]["udid"], devices[0]["state"]) if devices else print("", "")') if [ -n "$device" ]; then - [ "$state" = "Booted" ] || xcrun simctl boot "$device" - xcrun simctl bootstatus "$device" -b >/dev/null 2>&1 || true - xcrun simctl install "$device" "$app" - run_with_timeout xcrun simctl launch --console-pty --terminate-running-process "$device" "$BUNDLE_ID" \ - > "$LOGS/run-ios-sim.log" 2>&1 || true - [ "$state" = "Booted" ] || xcrun simctl shutdown "$device" || true + # A failure here leaves ios-sim at no-run in the table instead of ending the script under set -e. + if [ "$state" = "Booted" ] || xcrun simctl boot "$device"; then + xcrun simctl bootstatus "$device" -b >/dev/null 2>&1 || true + if xcrun simctl install "$device" "$app"; then + run_with_timeout xcrun simctl launch --console-pty --terminate-running-process "$device" "$BUNDLE_ID" \ + > "$LOGS/run-ios-sim.log" 2>&1 || true + else + echo "ios-sim: simctl install failed" >&2 + fi + [ "$state" = "Booted" ] || xcrun simctl shutdown "$device" || true + else + echo "ios-sim: simctl boot failed" >&2 + fi else echo "ios-sim: no available iPhone simulator" >&2 fi