diff --git a/.github/workflows/unity-compile-check-and-test-runner.yml b/.github/workflows/unity-compile-check-and-test-runner.yml index b62f2d8b7..976c8dff6 100644 --- a/.github/workflows/unity-compile-check-and-test-runner.yml +++ b/.github/workflows/unity-compile-check-and-test-runner.yml @@ -1,7 +1,7 @@ name: Unity Compile Check -# This job is a required status check, so it must report exactly one -# "Compile Check (Linux)" result on every PR. Path filters at the workflow +# These jobs are required status checks, so they must report one result per +# Unity version on every PR. Path filters at the workflow # level cannot express that: a `paths`/`paths-ignore` pair both fire on a PR # that mixes C# and non-C# files, producing duplicate required checks. Instead # the workflow always runs and a change-detection step decides whether to do @@ -17,8 +17,21 @@ permissions: jobs: compile-check: - name: Compile Check (Linux) + name: Compile Check (${{ matrix.unity-version }}) runs-on: ubuntu-latest + strategy: + matrix: + include: + - unity-version: 2022.3.62f1 + project-path: . + library-path: Library + package-warning-scope: false + run-unit-tests: true + - unity-version: 6000.5.4f1 + project-path: ci/unity-projects/6000.5 + library-path: ci/unity-projects/6000.5/Library + package-warning-scope: true + run-unit-tests: false # Dependabot PRs cannot access repository secrets (UNITY_LICENSE etc.) env: HAS_UNITY_LICENSE: ${{ secrets.UNITY_LICENSE != '' }} @@ -47,7 +60,7 @@ jobs: echo "=== Changed files ===" echo "$CHANGED" - if echo "$CHANGED" | grep -qE '^(Packages/src/Editor/.*\.cs|Packages/src/Runtime/.*\.cs|Assets/.*\.cs|tests/.*\.cs|tests/.*\.csproj|Packages/.*\.asmdef|Assets/.*\.asmdef|Packages/manifest\.json|Packages/packages-lock\.json|\.github/workflows/unity-compile-check-and-test-runner\.yml)$'; then + if echo "$CHANGED" | grep -qE '^(Packages/src/Editor/.*\.cs|Packages/src/Runtime/.*\.cs|Assets/.*\.cs|tests/.*\.cs|tests/.*\.csproj|Packages/.*\.asmdef|Assets/.*\.asmdef|Packages/manifest\.json|Packages/packages-lock\.json|ci/unity-projects/.*|\.github/workflows/unity-compile-check-and-test-runner\.yml)$'; then echo "csharp=true" >> "$GITHUB_OUTPUT" else echo "csharp=false" >> "$GITHUB_OUTPUT" @@ -58,13 +71,13 @@ jobs: run: echo "No C# changes detected; compile check is satisfied without running Unity." - name: Setup .NET - if: steps.changes.outputs.csharp == 'true' + if: steps.changes.outputs.csharp == 'true' && matrix.run-unit-tests == true uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 with: dotnet-version: 10.0.x - name: Run concurrency unit tests - if: steps.changes.outputs.csharp == 'true' + if: steps.changes.outputs.csharp == 'true' && matrix.run-unit-tests == true run: | dotnet test tests/DynamicCodeExecutionScheduler.UnitTests/DynamicCodeExecutionScheduler.UnitTests.csproj --configuration Release dotnet test tests/SharedRoslynCompilerWorker.UnitTests/SharedRoslynCompilerWorker.UnitTests.csproj --configuration Release @@ -74,13 +87,21 @@ jobs: if: env.HAS_UNITY_LICENSE == 'true' uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae with: - path: Library - key: Library-2022.3.62f1-${{ hashFiles('Packages/src/**/*.cs', 'Assets/**/*.cs', 'Packages/manifest.json', 'Packages/packages-lock.json') }} + path: ${{ matrix.library-path }} + key: Library-${{ matrix.unity-version }}-${{ hashFiles('Packages/src/**/*.cs', 'Assets/**/*.cs', 'Packages/manifest.json', 'Packages/packages-lock.json', 'ci/unity-projects/6000.5/**') }} restore-keys: | - Library-2022.3.62f1- + Library-${{ matrix.unity-version }}- - - name: Compile Unity project - if: steps.changes.outputs.csharp == 'true' && env.HAS_UNITY_LICENSE == 'true' + - name: Purge cached script assemblies + if: ${{ steps.changes.outputs.csharp == 'true' && env.HAS_UNITY_LICENSE == 'true' && matrix['unity-version'] == '6000.5.4f1' }} + run: rm -rf "${{ matrix.project-path }}/Library/ScriptAssemblies" + + - name: Prepare Unity log directory + if: env.HAS_UNITY_LICENSE == 'true' + run: mkdir -p artifacts + + - name: Compile Unity project (2022.3) + if: ${{ steps.changes.outputs.csharp == 'true' && env.HAS_UNITY_LICENSE == 'true' && matrix['unity-version'] == '2022.3.62f1' }} id: compile uses: game-ci/unity-builder@1d4ee0697f193f54668e98961d79907911f4b4f2 timeout-minutes: 30 @@ -89,12 +110,69 @@ jobs: UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} with: - unityVersion: 2022.3.62f1 + projectPath: ${{ matrix.project-path }} + unityVersion: ${{ matrix.unity-version }} targetPlatform: StandaloneLinux64 buildMethod: '' allowDirtyBuild: true continue-on-error: true + - name: Compile Unity project (6000.5) + if: ${{ steps.changes.outputs.csharp == 'true' && env.HAS_UNITY_LICENSE == 'true' && matrix['unity-version'] == '6000.5.4f1' }} + id: compile-6000-5 + timeout-minutes: 30 + env: + UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} + UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} + UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} + run: | + docker run --rm \ + --workdir /workspace \ + --volume "$GITHUB_WORKSPACE:/workspace" \ + --env UNITY_LICENSE \ + --env UNITY_EMAIL \ + --env UNITY_PASSWORD \ + unityci/editor:ubuntu-6000.5.4f1-base-3 \ + /bin/bash -c ' + set -eu + mkdir -p /BlankProject/Assets + trap '\''unity-editor -batchmode -nographics -quit -returnlicense \ + -logFile /dev/stdout \ + -username "$UNITY_EMAIL" -password "$UNITY_PASSWORD" \ + -projectPath /BlankProject >/dev/null 2>&1 || true'\'' EXIT + + LICENSE_VALUE=$(printf "%s" "$UNITY_LICENSE" | tr "\\n" " " | sed -n '\''s/.*.*/\1/p'\'') + test -n "$LICENSE_VALUE" + printf "%s" "$LICENSE_VALUE" | base64 -d > /tmp/developer-data.bin + SERIAL=$(tail -c +5 /tmp/developer-data.bin | tr -d "\\r\\n") + test "${#SERIAL}" -eq 27 + if [[ "$SERIAL" == F* ]]; then + dbus-uuidgen > /etc/machine-id + mkdir -p /var/lib/dbus + ln -sf /etc/machine-id /var/lib/dbus/machine-id + fi + + ACTIVATED=false + for ATTEMPT in 1 2 3; do + if unity-editor -batchmode -nographics -quit \ + -logFile /dev/stdout \ + -serial "$SERIAL" \ + -username "$UNITY_EMAIL" \ + -password "$UNITY_PASSWORD" \ + -projectPath /BlankProject; then + ACTIVATED=true + break + fi + sleep $((ATTEMPT * 5)) + done + test "$ACTIVATED" = true + + unity-editor -batchmode -nographics -quit \ + -projectPath /workspace/ci/unity-projects/6000.5 \ + -logFile /workspace/artifacts/unity-build.log + ' + continue-on-error: true + - name: Check for compile errors and warnings if: steps.changes.outputs.csharp == 'true' && env.HAS_UNITY_LICENSE == 'true' run: | @@ -110,14 +188,45 @@ jobs: ERROR_COUNT=0 WARNING_COUNT=0 + if [ "${{ matrix.package-warning-scope }}" == "true" ]; then + PACKAGE_ASSEMBLY_COUNT=$(find "${{ matrix.project-path }}/Library/ScriptAssemblies" -maxdepth 1 -type f -name 'UnityCLILoop*.dll' 2>/dev/null | wc -l | tr -d ' ') + if [ "$PACKAGE_ASSEMBLY_COUNT" -eq 0 ]; then + echo "::error::Unity did not resolve or compile the io.github.hatayama.uloopmcp package" + if [ -f "$LOG_FILE" ]; then + echo "=== Unity log tail ===" + tail -100 "$LOG_FILE" + else + echo "::error::Unity log file is missing: $LOG_FILE" + fi + exit 1 + fi + echo "Package assembly evidence found: $PACKAGE_ASSEMBLY_COUNT assembly(s)." + fi + if [ -f "$LOG_FILE" ]; then echo "Analyzing log file: $LOG_FILE" - # Count errors (CS errors from compiler) - ERROR_COUNT=$(grep -c "error CS[0-9]*:" "$LOG_FILE" 2>/dev/null || echo "0") + if [ "${{ matrix.package-warning-scope }}" == "true" ]; then + PACKAGE_EVIDENCE_PATTERN='io\.github\.hatayama\.uloopmcp|UnityCLILoop\.Runtime' + if ! grep -Eq "$PACKAGE_EVIDENCE_PATTERN" "$LOG_FILE"; then + echo "::error::Unity did not resolve or compile the io.github.hatayama.uloopmcp package" + exit 1 + fi + echo "Package compilation evidence found in Unity log." + fi + + if [ "${{ matrix.package-warning-scope }}" == "true" ]; then + # Unity 6000.5 can emit compiler noise from other packages. Only + # package source diagnostics determine this compatibility check. + ERROR_PATTERN='(Packages/src/|io\.github\.hatayama\.uloopmcp).*error CS[0-9]*:|error CS[0-9]*:.*(Packages/src/|io\.github\.hatayama\.uloopmcp)' + WARNING_PATTERN='(Packages/src/|io\.github\.hatayama\.uloopmcp).*warning CS[0-9]*:|warning CS[0-9]*:.*(Packages/src/|io\.github\.hatayama\.uloopmcp)' + else + ERROR_PATTERN='error CS[0-9]*:' + WARNING_PATTERN='warning CS[0-9]*:' + fi - # Count warnings (CS warnings from compiler, excluding specific noise) - WARNING_COUNT=$(grep -c "warning CS[0-9]*:" "$LOG_FILE" 2>/dev/null || echo "0") + ERROR_COUNT=$(grep -Ec "$ERROR_PATTERN" "$LOG_FILE" 2>/dev/null || true) + WARNING_COUNT=$(grep -Ec "$WARNING_PATTERN" "$LOG_FILE" 2>/dev/null || true) echo "" echo "=== Compilation Results ===" @@ -129,7 +238,7 @@ jobs: echo "::error::Found $ERROR_COUNT compile error(s)" echo "" echo "=== Errors ===" - grep "error CS[0-9]*:" "$LOG_FILE" | head -20 + grep -E "$ERROR_PATTERN" "$LOG_FILE" | head -20 exit 1 fi @@ -137,15 +246,15 @@ jobs: echo "::warning::Found $WARNING_COUNT compile warning(s)" echo "" echo "=== Warnings ===" - grep "warning CS[0-9]*:" "$LOG_FILE" | head -20 + grep -E "$WARNING_PATTERN" "$LOG_FILE" | head -20 exit 1 fi echo "✅ No compile errors or warnings found!" else echo "::warning::Could not find Unity log file for analysis" - # Fall back to checking if build step failed - if [ "${{ steps.compile.outcome }}" == "failure" ]; then + # Unity 6000.5's builder reports an untitled-scene build failure after successful script compilation. + if [ "${{ matrix.package-warning-scope }}" == "true" ] || [ "${{ steps.compile.outcome }}" == "failure" ]; then echo "::error::Unity compilation failed" exit 1 fi diff --git a/Packages/src/Editor/FirstPartyTools/Common/MouseUi/UiRaycastHelper.cs b/Packages/src/Editor/FirstPartyTools/Common/MouseUi/UiRaycastHelper.cs index 166447a8c..ea15937d0 100644 --- a/Packages/src/Editor/FirstPartyTools/Common/MouseUi/UiRaycastHelper.cs +++ b/Packages/src/Editor/FirstPartyTools/Common/MouseUi/UiRaycastHelper.cs @@ -28,7 +28,11 @@ internal static class UiRaycastHelper private static List CollectCanvasRaycastSources() { +#if UNITY_6000_4_OR_NEWER + Canvas[] canvases = Object.FindObjectsByType(); +#else Canvas[] canvases = Object.FindObjectsByType(FindObjectsSortMode.None); +#endif List canvasRaycastSources = new List(); foreach (Canvas canvas in canvases) diff --git a/Packages/src/Editor/FirstPartyTools/Common/Overlay/OverlayCanvasFactory.cs b/Packages/src/Editor/FirstPartyTools/Common/Overlay/OverlayCanvasFactory.cs index aa9f351c5..486649be2 100644 --- a/Packages/src/Editor/FirstPartyTools/Common/Overlay/OverlayCanvasFactory.cs +++ b/Packages/src/Editor/FirstPartyTools/Common/Overlay/OverlayCanvasFactory.cs @@ -38,8 +38,12 @@ public void EnsureExists() } // Domain Reload resets _instance but DontDestroyOnLoad objects survive; reclaim one and destroy duplicates +#if UNITY_6000_4_OR_NEWER + InputVisualizationCanvas[] existing = Object.FindObjectsByType(); +#else InputVisualizationCanvas[] existing = Object.FindObjectsByType(FindObjectsSortMode.None); +#endif for (int i = 0; i < existing.Length; i++) { if (_instance == null) diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/AssemblyBuilderFallbackCompilerBackend.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/AssemblyBuilderFallbackCompilerBackend.cs index 570a2e065..998e6f665 100644 --- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/AssemblyBuilderFallbackCompilerBackend.cs +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/AssemblyBuilderFallbackCompilerBackend.cs @@ -28,11 +28,13 @@ public static async Task CompileAsync( ? DynamicReferenceSetBuilder.MergeReferencesByAssemblyName(Array.Empty(), references) : Array.Empty(); +#pragma warning disable 618 // Unity's fallback compiler API has no supported replacement on older editor versions. AssemblyBuilder builder = new(dllPath, sourcePath) { referencesOptions = ReferencesOptions.UseEngineModules, additionalReferences = referenceArray }; +#pragma warning restore 618 builder.buildFinished += (string assemblyPath, CompilerMessage[] compilerMessages) => { diff --git a/Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointUnityObjectClassifier.cs b/Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointUnityObjectClassifier.cs index 6b683f4e8..c4f3705f0 100644 --- a/Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointUnityObjectClassifier.cs +++ b/Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointUnityObjectClassifier.cs @@ -2,6 +2,7 @@ using UnityEngine; using io.github.hatayama.UnityCliLoop.Runtime; +using io.github.hatayama.UnityCliLoop.ToolContracts; namespace io.github.hatayama.UnityCliLoop.FirstPartyTools { @@ -43,9 +44,9 @@ public static Classification Classify(Object unityObject) string assetPath = AssetDatabase.GetAssetPath(unityObject); return string.IsNullOrEmpty(assetPath) ? new Classification( - UloopCapturedVariableUnityObjectKind.RuntimeInstance, unityObject.name, unityObject.GetInstanceID()) + UloopCapturedVariableUnityObjectKind.RuntimeInstance, unityObject.name, UnityObjectIdentifier.GetInstanceId(unityObject)) : new Classification( - UloopCapturedVariableUnityObjectKind.Asset, assetPath, unityObject.GetInstanceID()); + UloopCapturedVariableUnityObjectKind.Asset, assetPath, UnityObjectIdentifier.GetInstanceId(unityObject)); } private static Classification ClassifyGameObjectOrComponent(GameObject gameObject, Object handleSource) @@ -55,13 +56,13 @@ private static Classification ClassifyGameObjectOrComponent(GameObject gameObjec return new Classification( UloopCapturedVariableUnityObjectKind.SceneObject, $"{gameObject.scene.name}:{BuildHierarchyPath(gameObject.transform)}", - handleSource.GetInstanceID()); + UnityObjectIdentifier.GetInstanceId(handleSource)); } return new Classification( UloopCapturedVariableUnityObjectKind.PrefabAsset, AssetDatabase.GetAssetPath(handleSource), - handleSource.GetInstanceID()); + UnityObjectIdentifier.GetInstanceId(handleSource)); } private static string BuildHierarchyPath(Transform transform) diff --git a/Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointVariableFormatter.cs b/Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointVariableFormatter.cs index be9ec6976..aa723531b 100644 --- a/Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointVariableFormatter.cs +++ b/Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointVariableFormatter.cs @@ -81,7 +81,7 @@ private static UloopCapturedVariable FormatUnityObjectVariable( { return new UloopCapturedVariable( name, scope, typeName, DestroyedValue, - UloopCapturedVariableUnityObjectKind.Destroyed, string.Empty, unityObjectCandidate.GetInstanceID()); + UloopCapturedVariableUnityObjectKind.Destroyed, string.Empty, UnityObjectIdentifier.GetInstanceId(unityObjectCandidate)); } SourcePausePointUnityObjectClassifier.Classification classification = diff --git a/Packages/src/Editor/FirstPartyTools/Screenshot/RaycastAnnotation/RaycastGridAnnotator.cs b/Packages/src/Editor/FirstPartyTools/Screenshot/RaycastAnnotation/RaycastGridAnnotator.cs index 8c30b6aa0..7b806336e 100644 --- a/Packages/src/Editor/FirstPartyTools/Screenshot/RaycastAnnotation/RaycastGridAnnotator.cs +++ b/Packages/src/Editor/FirstPartyTools/Screenshot/RaycastAnnotation/RaycastGridAnnotator.cs @@ -189,7 +189,7 @@ internal static int CreateClusterKey(Collider collider) // The 4-connected component split in CollectPhysicsColliderElements re-derives one annotation per // visually closed region so a single GameObject can produce multiple entries when UI occlusion // splits its reachable samples. - return collider!.gameObject.GetInstanceID(); + return UnityObjectIdentifier.GetInstanceId(collider!.gameObject); } private static RaycastClusterSample CreateClusterSample( diff --git a/Packages/src/Editor/FirstPartyTools/Screenshot/UIElementAnnotator.cs b/Packages/src/Editor/FirstPartyTools/Screenshot/UIElementAnnotator.cs index 77569a572..635faa518 100644 --- a/Packages/src/Editor/FirstPartyTools/Screenshot/UIElementAnnotator.cs +++ b/Packages/src/Editor/FirstPartyTools/Screenshot/UIElementAnnotator.cs @@ -122,7 +122,11 @@ private static void CollectEventHandlers( HashSet processedObjects, UiRaycastHelper.RaycastContext? raycastContext) { +#if UNITY_6000_4_OR_NEWER + MonoBehaviour[] allBehaviours = Object.FindObjectsByType(); +#else MonoBehaviour[] allBehaviours = Object.FindObjectsOfType(); +#endif foreach (MonoBehaviour behaviour in allBehaviours) { if (!behaviour.isActiveAndEnabled) diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiPointerTargetResolver.cs b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiPointerTargetResolver.cs index d588d8e5f..2dedc3bd7 100644 --- a/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiPointerTargetResolver.cs +++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiPointerTargetResolver.cs @@ -185,7 +185,11 @@ private static TargetPathLookupResult FindActiveGameObjectByPath(string targetPa return new TargetPathLookupResult(null, 0); } +#if UNITY_6000_4_OR_NEWER + GameObject[] gameObjects = UnityEngine.Object.FindObjectsByType(); +#else GameObject[] gameObjects = UnityEngine.Object.FindObjectsByType(FindObjectsSortMode.None); +#endif GameObject? matchedTarget = null; int matchCount = 0; diff --git a/Packages/src/Editor/ToolContracts/UnityObjectIdentifier.cs b/Packages/src/Editor/ToolContracts/UnityObjectIdentifier.cs new file mode 100644 index 000000000..c5c9b2207 --- /dev/null +++ b/Packages/src/Editor/ToolContracts/UnityObjectIdentifier.cs @@ -0,0 +1,25 @@ +using UnityEngine; + +namespace io.github.hatayama.UnityCliLoop.ToolContracts +{ + /// + /// Provides the integer object handle required by the current pause-point response contract. + /// + internal static class UnityObjectIdentifier + { + /// + /// Gets an object handle while keeping the existing integer response contract compatible with older Unity versions. + /// + public static int GetInstanceId(Object unityObject) + { + Debug.Assert(unityObject is not null, "unityObject must not be null."); +#if UNITY_6000_4_OR_NEWER + // The wire contract remains int, so extract the same lower 32 bits as Unity's obsolete int cast. + // Unity 6000.5 stores a version marker in the upper 32 bits; future IDs may collide in this int shape. + return unchecked((int)EntityId.ToULong(unityObject.GetEntityId())); +#else + return unityObject.GetInstanceID(); +#endif + } + } +} diff --git a/Packages/src/Editor/ToolContracts/UnityObjectIdentifier.cs.meta b/Packages/src/Editor/ToolContracts/UnityObjectIdentifier.cs.meta new file mode 100644 index 000000000..d5526d53d --- /dev/null +++ b/Packages/src/Editor/ToolContracts/UnityObjectIdentifier.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 6c0079dd96933453285941f49016354c diff --git a/ci/unity-projects/6000.5/Assets/.gitkeep b/ci/unity-projects/6000.5/Assets/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/ci/unity-projects/6000.5/Packages/manifest.json b/ci/unity-projects/6000.5/Packages/manifest.json new file mode 100644 index 000000000..512a47c96 --- /dev/null +++ b/ci/unity-projects/6000.5/Packages/manifest.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "io.github.hatayama.uloopmcp": "file:../../../../Packages/src" + } +} diff --git a/ci/unity-projects/6000.5/ProjectSettings/ProjectVersion.txt b/ci/unity-projects/6000.5/ProjectSettings/ProjectVersion.txt new file mode 100644 index 000000000..c40a41609 --- /dev/null +++ b/ci/unity-projects/6000.5/ProjectSettings/ProjectVersion.txt @@ -0,0 +1 @@ +m_EditorVersion: 6000.5.4f1