Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions .claude/skills/asp-ft-player-builds/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
---
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-<version>`. The script builds the
package of its own checkout.

```bash
scripts/player-builds.sh <scratch>/player-builds [unity-version]
```

- `<scratch>/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 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

```
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<int>`: 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`: 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,
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.
5 changes: 3 additions & 2 deletions .claude/skills/asp-ft-release/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions Aspid.FastTools/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <dir> [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 <dir> [unity-version]`, read by the `asp-ft-player-builds` skill.
- YAML engine: `dotnet test DotnetTests/Yaml`.
- Type resolver: `dotnet test DotnetTests/Types`.
Original file line number Diff line number Diff line change
@@ -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
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions Aspid.FastTools/Assets/PlayerBuildCheck~/Editor.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -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
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

170 changes: 170 additions & 0 deletions Aspid.FastTools/Assets/PlayerBuildCheck~/Editor/PlayerBuilds.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
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 <dir>
// Builds every target without BuildOptions.Development, appends a line per target to <dir>/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 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;
// 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);
}
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;
Comment thread
VPDPersonal marked this conversation as resolved.
Resolved

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<PlayerBuildProbe>();

// 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<int>).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} <value> 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; }
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading