diff --git a/MCPForUnity/Editor/Tools/Build/BuildTargetMapping.cs b/MCPForUnity/Editor/Tools/Build/BuildTargetMapping.cs index 8377bb52e..43383c672 100644 --- a/MCPForUnity/Editor/Tools/Build/BuildTargetMapping.cs +++ b/MCPForUnity/Editor/Tools/Build/BuildTargetMapping.cs @@ -1,3 +1,4 @@ +using System; using UnityEditor; using UnityEditor.Build; @@ -5,6 +6,8 @@ namespace MCPForUnity.Editor.Tools.Build { public static class BuildTargetMapping { + private const string VisionOSName = "VisionOS"; + public static bool TryResolveBuildTarget(string name, out BuildTarget target) { if (string.IsNullOrEmpty(name)) @@ -24,13 +27,12 @@ public static bool TryResolveBuildTarget(string name, out BuildTarget target) case "webgl": target = BuildTarget.WebGL; return true; case "uwp": target = BuildTarget.WSAPlayer; return true; case "tvos": target = BuildTarget.tvOS; return true; - // BuildTarget.VisionOS exists only in Unity 2023.2+ and late 2022.3 patches -#if UNITY_2023_2_OR_NEWER - case "visionos": target = BuildTarget.VisionOS; return true; -#endif default: - if (System.Enum.TryParse(name, true, out target)) + if (TryParseDefinedBuildTarget(name, out target)) + { return true; + } + target = default; return false; } @@ -50,10 +52,14 @@ public static BuildTargetGroup GetTargetGroup(BuildTarget target) case BuildTarget.WebGL: return BuildTargetGroup.WebGL; case BuildTarget.WSAPlayer: return BuildTargetGroup.WSA; case BuildTarget.tvOS: return BuildTargetGroup.tvOS; -#if UNITY_2023_2_OR_NEWER - case BuildTarget.VisionOS: return BuildTargetGroup.VisionOS; -#endif - default: return BuildTargetGroup.Unknown; + default: + if (IsVisionOSTarget(target) + && Enum.TryParse(VisionOSName, true, out BuildTargetGroup visionOSGroup)) + { + return visionOSGroup; + } + + return BuildTargetGroup.Unknown; } } @@ -67,12 +73,61 @@ public static string TryResolveNamedBuildTarget(string name, out NamedBuildTarge if (!TryResolveBuildTarget(name, out var buildTarget)) { namedTarget = default; - return $"Unknown build target: '{name}'. Valid targets: windows64, osx, linux64, android, ios, webgl, uwp, tvos, visionos"; + return GetUnknownBuildTargetMessage(name); + } + + var targetGroup = GetTargetGroup(buildTarget); + if (targetGroup == BuildTargetGroup.Unknown) + { + namedTarget = default; + return IsVisionOSTarget(buildTarget) + ? "VisionOS build target is available, but its BuildTargetGroup is not exposed by this Unity editor installation." + : $"Build target group could not be resolved for target '{buildTarget}'."; } - namedTarget = GetNamedBuildTarget(buildTarget); + + namedTarget = NamedBuildTarget.FromBuildTargetGroup(targetGroup); return null; } + public static string GetUnknownBuildTargetMessage(string name) + { + if (string.Equals(name, "visionos", StringComparison.OrdinalIgnoreCase)) + { + return "VisionOS build target is not available in this Unity editor installation. " + + "Install the visionOS build support module or use a Unity version/configuration that exposes BuildTarget.VisionOS."; + } + + return $"Unknown build target: '{name}'. Valid targets: {GetValidTargetsList()}."; + } + + private static string GetValidTargetsList() + { + string validTargets = "windows64, osx, linux64, android, ios, webgl, uwp, tvos"; + if (TryParseDefinedBuildTarget(VisionOSName, out _)) + { + validTargets += ", visionos"; + } + + return validTargets; + } + + private static bool IsVisionOSTarget(BuildTarget target) + { + return string.Equals(target.ToString(), VisionOSName, StringComparison.OrdinalIgnoreCase); + } + + private static bool TryParseDefinedBuildTarget(string name, out BuildTarget target) + { + target = default; + if (int.TryParse(name, out _)) + { + return false; + } + + return Enum.TryParse(name, true, out target) + && Enum.IsDefined(typeof(BuildTarget), target); + } + public static string GetDefaultOutputPath(BuildTarget target, string productName) { string basePath = $"Builds/{target}"; diff --git a/MCPForUnity/Editor/Tools/ManageBuild.cs b/MCPForUnity/Editor/Tools/ManageBuild.cs index ad2f5d75c..8b5c89ac3 100644 --- a/MCPForUnity/Editor/Tools/ManageBuild.cs +++ b/MCPForUnity/Editor/Tools/ManageBuild.cs @@ -64,7 +64,7 @@ private static object HandleBuild(ToolParams p) string targetName = p.Get("target"); if (!BuildTargetMapping.TryResolveBuildTarget(targetName, out var target)) - return new ErrorResponse($"Unknown target '{targetName}'."); + return new ErrorResponse(BuildTargetMapping.GetUnknownBuildTargetMessage(targetName)); var group = BuildTargetMapping.GetTargetGroup(target); if (!BuildPipeline.IsBuildTargetSupported(group, target)) @@ -221,7 +221,7 @@ private static object HandlePlatform(ToolParams p) // Switch platform if (!BuildTargetMapping.TryResolveBuildTarget(targetName, out var target)) - return new ErrorResponse($"Unknown target '{targetName}'."); + return new ErrorResponse(BuildTargetMapping.GetUnknownBuildTargetMessage(targetName)); var group = BuildTargetMapping.GetTargetGroup(target); if (!BuildPipeline.IsBuildTargetSupported(group, target)) @@ -442,7 +442,7 @@ private static object HandleBatch(ToolParams p) foreach (var t in targets) { if (!BuildTargetMapping.TryResolveBuildTarget(t, out var bt)) - return new ErrorResponse($"Unknown target '{t}' in batch."); + return new ErrorResponse(BuildTargetMapping.GetUnknownBuildTargetMessage(t)); var btGroup = BuildTargetMapping.GetTargetGroup(bt); if (!BuildPipeline.IsBuildTargetSupported(btGroup, bt)) return new ErrorResponse( diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/BuildTargetMappingTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/BuildTargetMappingTests.cs new file mode 100644 index 000000000..0e1a55219 --- /dev/null +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/BuildTargetMappingTests.cs @@ -0,0 +1,67 @@ +using System; +using MCPForUnity.Editor.Tools.Build; +using NUnit.Framework; +using UnityEditor; + +namespace MCPForUnity.Tests.EditMode.Tools +{ + [TestFixture] + public class BuildTargetMappingTests + { + [TestCase("windows64", BuildTarget.StandaloneWindows64)] + [TestCase("macos", BuildTarget.StandaloneOSX)] + [TestCase("linux", BuildTarget.StandaloneLinux64)] + [TestCase("tvos", BuildTarget.tvOS)] + public void TryResolveBuildTarget_KnownAliasesResolve(string name, BuildTarget expected) + { + Assert.IsTrue(BuildTargetMapping.TryResolveBuildTarget(name, out var target)); + Assert.AreEqual(expected, target); + } + + [Test] + public void TryResolveBuildTarget_NumericInputDoesNotResolve() + { + Assert.IsFalse(BuildTargetMapping.TryResolveBuildTarget("5", out _)); + } + + [Test] + public void TryResolveNamedBuildTarget_UnknownTargetListsOnlyAvailableTargets() + { + string error = BuildTargetMapping.TryResolveNamedBuildTarget("not-a-target", out _); + + Assert.IsNotNull(error); + StringAssert.Contains("windows64", error); + + bool visionOSAvailable = Enum.TryParse("VisionOS", true, out BuildTarget _); + if (visionOSAvailable) + { + StringAssert.Contains("visionos", error); + } + else + { + Assert.IsFalse(error.Contains("visionos")); + } + } + + [Test] + public void TryResolveNamedBuildTarget_VisionOSUnavailableReturnsHelpfulError() + { + bool visionOSAvailable = Enum.TryParse("VisionOS", true, out BuildTarget _); + string error = BuildTargetMapping.TryResolveNamedBuildTarget("visionos", out _); + + if (visionOSAvailable) + { + Assert.IsTrue(BuildTargetMapping.TryResolveBuildTarget("visionos", out _)); + Assert.IsTrue( + error == null || error.Contains("VisionOS"), + $"Expected no error or a VisionOS-specific error, got: {error}"); + } + else + { + Assert.IsFalse(BuildTargetMapping.TryResolveBuildTarget("visionos", out _)); + Assert.IsNotNull(error); + StringAssert.Contains("VisionOS build target is not available", error); + } + } + } +} diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/BuildTargetMappingTests.cs.meta b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/BuildTargetMappingTests.cs.meta new file mode 100644 index 000000000..35e6c9c6e --- /dev/null +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/BuildTargetMappingTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 94419cfeebb59e84ea16331766eaad52 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: