chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:49:17 +08:00
commit 7243d5823b
2201 changed files with 257291 additions and 0 deletions
+189
View File
@@ -0,0 +1,189 @@
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.Build.Reporting;
namespace MCPForUnity.Editor.Tools.Build
{
public enum BuildJobState
{
Pending,
Building,
Succeeded,
Failed,
Cancelled,
Skipped
}
public class BuildJob
{
public string JobId { get; }
public BuildJobState State { get; set; } = BuildJobState.Pending;
public BuildTarget Target { get; set; }
public string OutputPath { get; set; }
public DateTime StartedAt { get; set; }
public DateTime? CompletedAt { get; set; }
public string ErrorMessage { get; set; }
// Extracted from BuildReport at completion to avoid retaining the heavy native object
public double TotalSizeMb { get; set; }
public int TotalErrors { get; set; }
public int TotalWarnings { get; set; }
public BuildJob(string jobId, BuildTarget target, string outputPath)
{
JobId = jobId;
Target = target;
OutputPath = outputPath;
}
public object ToStatusResponse()
{
var data = new Dictionary<string, object>
{
["job_id"] = JobId,
["result"] = State.ToString().ToLowerInvariant(),
["platform"] = Target.ToString(),
["output_path"] = OutputPath
};
if (StartedAt != default)
data["started_at"] = StartedAt.ToString("O");
if (CompletedAt.HasValue)
{
data["duration_seconds"] = (CompletedAt.Value - StartedAt).TotalSeconds;
data["completed_at"] = CompletedAt.Value.ToString("O");
}
if (State == BuildJobState.Succeeded || State == BuildJobState.Failed)
{
data["total_size_mb"] = TotalSizeMb;
data["errors"] = TotalErrors;
data["warnings"] = TotalWarnings;
}
if (!string.IsNullOrEmpty(ErrorMessage))
data["error"] = ErrorMessage;
return data;
}
}
public class BatchJob
{
public string JobId { get; }
public BuildJobState State { get; set; } = BuildJobState.Pending;
public List<BuildJob> Children { get; } = new();
public int CurrentIndex { get; set; } = -1;
public BatchJob(string jobId)
{
JobId = jobId;
}
public object ToStatusResponse()
{
int completed = 0;
string currentBuild = null;
var builds = new List<object>();
foreach (var child in Children)
{
if (child.State == BuildJobState.Succeeded || child.State == BuildJobState.Failed
|| child.State == BuildJobState.Skipped || child.State == BuildJobState.Cancelled)
completed++;
if (child.State == BuildJobState.Building)
currentBuild = child.JobId;
builds.Add(child.ToStatusResponse());
}
return new Dictionary<string, object>
{
["job_id"] = JobId,
["result"] = State.ToString().ToLowerInvariant(),
["completed"] = completed,
["total"] = Children.Count,
["current_build"] = currentBuild,
["builds"] = builds
};
}
}
/// <summary>
/// Static store for all build jobs. Note: static fields are cleared on domain reload,
/// but this is acceptable because BuildPipeline.BuildPlayer blocks the editor thread,
/// preventing domain reload during a build. For batch builds with platform switches,
/// the batch scheduling happens after each build completes via EditorApplication.update
/// callbacks (ScheduleOnNextUpdate / WaitForCompletion), so state is maintained within
/// a single domain lifecycle.
/// </summary>
public static class BuildJobStore
{
private static readonly Dictionary<string, BuildJob> _buildJobs = new();
private static readonly Dictionary<string, BatchJob> _batchJobs = new();
private static BuildJob _lastCompletedJob;
public static string CreateJobId() => $"build-{Guid.NewGuid():N}".Substring(0, 16);
public static string CreateBatchId() => $"batch-{Guid.NewGuid():N}".Substring(0, 16);
public static void AddBuildJob(BuildJob job) => _buildJobs[job.JobId] = job;
public static void AddBatchJob(BatchJob job) => _batchJobs[job.JobId] = job;
public static BuildJob GetBuildJob(string jobId)
{
_buildJobs.TryGetValue(jobId, out var job);
return job;
}
public static BatchJob GetBatchJob(string jobId)
{
_batchJobs.TryGetValue(jobId, out var job);
return job;
}
public static BuildJob LastCompletedJob => _lastCompletedJob;
public static void SetLastCompleted(BuildJob job)
{
_lastCompletedJob = job;
PruneOldJobs();
}
private const int MaxRetainedJobs = 50;
private static void PruneOldJobs()
{
if (_buildJobs.Count <= MaxRetainedJobs) return;
var toRemove = new List<string>();
foreach (var kvp in _buildJobs)
{
if (kvp.Value.State != BuildJobState.Building && kvp.Value.State != BuildJobState.Pending
&& kvp.Value != _lastCompletedJob)
toRemove.Add(kvp.Key);
}
foreach (var key in toRemove)
{
_buildJobs.Remove(key);
if (_buildJobs.Count <= MaxRetainedJobs / 2) break;
}
// Also prune batch jobs whose children are all terminal
var batchesToRemove = new List<string>();
foreach (var kvp in _batchJobs)
{
var batch = kvp.Value;
if (batch.State == BuildJobState.Building || batch.State == BuildJobState.Pending)
continue;
// Remove child references that were already pruned from _buildJobs
batch.Children.RemoveAll(c => !_buildJobs.ContainsKey(c.JobId));
if (batch.Children.Count == 0)
batchesToRemove.Add(kvp.Key);
}
foreach (var key in batchesToRemove)
_batchJobs.Remove(key);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6c04470155554320bc9e1655674d6aa7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,234 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditor.Build.Reporting;
using UnityEditor.Build;
using MCPForUnity.Editor.Helpers;
namespace MCPForUnity.Editor.Tools.Build
{
public static class BuildRunner
{
public static object ScheduleBuild(BuildJob job, BuildPlayerOptions options)
{
job.State = BuildJobState.Pending;
BuildJobStore.AddBuildJob(job);
ScheduleOnNextUpdate(() =>
RunBuildCore(job, () => BuildPipeline.BuildPlayer(options)));
return new PendingResponse(
$"Build scheduled for {job.Target}. Polling for completion...",
pollIntervalSeconds: 5.0,
data: new { job_id = job.JobId, platform = job.Target.ToString() }
);
}
#if UNITY_6000_0_OR_NEWER
public static object ScheduleProfileBuild(BuildJob job, BuildPlayerWithProfileOptions options)
{
job.State = BuildJobState.Pending;
BuildJobStore.AddBuildJob(job);
ScheduleOnNextUpdate(() =>
RunBuildCore(job, () => BuildPipeline.BuildPlayer(options)));
return new PendingResponse(
$"Profile build scheduled for {job.Target}. Polling for completion...",
pollIntervalSeconds: 5.0,
data: new { job_id = job.JobId, platform = job.Target.ToString() }
);
}
#endif
public static BuildPlayerOptions CreateBuildOptions(
BuildTarget target,
string outputPath,
string[] scenes,
BuildOptions buildOptions,
int subtarget)
{
var options = new BuildPlayerOptions
{
target = target,
targetGroup = BuildTargetMapping.GetTargetGroup(target),
locationPathName = outputPath,
scenes = scenes ?? GetDefaultScenes(),
options = buildOptions,
};
// Subtarget is only meaningful for Standalone (Server vs Player).
// On Android/iOS it maps to MobileTextureSubtarget (texture compression format).
// Passing StandaloneBuildSubtarget.Player (0) for mobile platforms forces
// a specific texture format — confirmed Unity bug IN-102413 where value 0
// on Unity 6000+ triggers PVRTC, ignoring Player Settings.
// Leave at platform default (0) so Unity respects Player Settings.
if (target == BuildTarget.StandaloneWindows
|| target == BuildTarget.StandaloneWindows64
|| target == BuildTarget.StandaloneOSX
|| target == BuildTarget.StandaloneLinux64)
{
options.subtarget = subtarget;
}
return options;
}
public static BuildOptions ParseBuildOptions(string[] optionNames, bool development)
{
var opts = BuildOptions.None;
if (development)
opts |= BuildOptions.Development;
if (optionNames == null) return opts;
foreach (var name in optionNames)
{
switch (name.ToLowerInvariant())
{
case "clean_build": opts |= BuildOptions.CleanBuildCache; break;
case "auto_run": opts |= BuildOptions.AutoRunPlayer; break;
case "deep_profiling": opts |= BuildOptions.EnableDeepProfilingSupport; break;
case "compress_lz4": opts |= BuildOptions.CompressWithLz4; break;
case "strict_mode": opts |= BuildOptions.StrictMode; break;
case "detailed_report": opts |= BuildOptions.DetailedBuildReport; break;
case "allow_debugging": opts |= BuildOptions.AllowDebugging; break;
case "connect_profiler": opts |= BuildOptions.ConnectWithProfiler; break;
case "scripts_only": opts |= BuildOptions.BuildScriptsOnly; break;
case "show_player": opts |= BuildOptions.ShowBuiltPlayer; break;
case "include_tests": opts |= BuildOptions.IncludeTestAssemblies; break;
}
}
return opts;
}
private static void RunBuildCore(BuildJob job, Func<BuildReport> buildFunc)
{
job.State = BuildJobState.Building;
job.StartedAt = DateTime.UtcNow;
try
{
BuildReport report = buildFunc();
job.CompletedAt = DateTime.UtcNow;
// Extract summary data immediately, then let the BuildReport be GC'd
var summary = report.summary;
job.TotalSizeMb = Math.Round(summary.totalSize / (1024.0 * 1024.0), 2);
job.TotalErrors = summary.totalErrors;
job.TotalWarnings = summary.totalWarnings;
if (summary.result == BuildResult.Succeeded)
{
job.State = BuildJobState.Succeeded;
}
else
{
job.State = BuildJobState.Failed;
#if UNITY_2023_1_OR_NEWER
job.ErrorMessage = report.SummarizeErrors();
#else
job.ErrorMessage = $"Build failed with result: {summary.result}";
#endif
}
}
catch (Exception ex)
{
job.State = BuildJobState.Failed;
job.CompletedAt = DateTime.UtcNow;
job.ErrorMessage = ex.Message;
}
BuildJobStore.SetLastCompleted(job);
// Emit console signal so the build result is visible in Unity's Console
if (job.State == BuildJobState.Succeeded)
{
UnityEngine.Debug.Log(
$"[MCP Build] Build succeeded: {job.Target} → {job.OutputPath} " +
$"({job.TotalSizeMb} MB, {(job.CompletedAt.Value - job.StartedAt).TotalSeconds:F1}s)");
}
else
{
UnityEngine.Debug.LogError(
$"[MCP Build] ✗ Build failed: {job.Target} — {job.ErrorMessage}");
}
}
public static void ScheduleNextBatchBuild(BatchJob batch, Func<int, BuildJob> createChildBuild)
{
batch.CurrentIndex++;
if (batch.State == BuildJobState.Cancelled)
{
for (int i = batch.CurrentIndex; i < batch.Children.Count; i++)
batch.Children[i].State = BuildJobState.Skipped;
return;
}
if (batch.CurrentIndex >= batch.Children.Count)
{
bool anyFailed = batch.Children.Any(c => c.State == BuildJobState.Failed);
batch.State = anyFailed ? BuildJobState.Failed : BuildJobState.Succeeded;
return;
}
var child = batch.Children[batch.CurrentIndex];
createChildBuild(batch.CurrentIndex);
// Safety timeout: if child never transitions out of Building/Pending after 2 hours,
// unregister the delegate to prevent an orphaned update loop
var watchStart = DateTime.UtcNow;
var maxWait = TimeSpan.FromHours(2);
void WaitForCompletion()
{
if (DateTime.UtcNow - watchStart > maxWait)
{
EditorApplication.update -= WaitForCompletion;
if (child.State == BuildJobState.Building || child.State == BuildJobState.Pending)
{
child.State = BuildJobState.Failed;
child.ErrorMessage = "Build timed out after 2 hours.";
}
ScheduleNextBatchBuild(batch, createChildBuild);
return;
}
if (child.State == BuildJobState.Building || child.State == BuildJobState.Pending)
return;
EditorApplication.update -= WaitForCompletion;
ScheduleNextBatchBuild(batch, createChildBuild);
}
EditorApplication.update += WaitForCompletion;
}
/// <summary>
/// Schedule an action to run on the next EditorApplication.update tick.
/// Unlike delayCall, update callbacks are guaranteed to fire since the
/// TransportCommandDispatcher processes commands through the same mechanism.
/// </summary>
private static void ScheduleOnNextUpdate(Action action)
{
bool executed = false;
void RunOnce()
{
if (executed) return;
executed = true;
EditorApplication.update -= RunOnce;
action();
}
EditorApplication.update += RunOnce;
}
private static string[] GetDefaultScenes()
{
return EditorBuildSettings.scenes
.Where(s => s.enabled)
.Select(s => s.path)
.ToArray();
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9e1fff59ee9947f49ad112a9403b11df
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,94 @@
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.Build;
namespace MCPForUnity.Editor.Tools.Build
{
public static class BuildSettingsHelper
{
public static object ReadProperty(string property, NamedBuildTarget namedTarget)
{
switch (property.ToLowerInvariant())
{
case "product_name":
return new { property, value = PlayerSettings.productName };
case "company_name":
return new { property, value = PlayerSettings.companyName };
case "version":
return new { property, value = PlayerSettings.bundleVersion };
case "bundle_id":
return new { property, value = PlayerSettings.GetApplicationIdentifier(namedTarget) };
case "scripting_backend":
var backend = PlayerSettings.GetScriptingBackend(namedTarget);
return new { property, value = backend == ScriptingImplementation.IL2CPP ? "il2cpp" : "mono" };
case "defines":
return new { property, value = PlayerSettings.GetScriptingDefineSymbols(namedTarget) };
case "architecture":
var arch = PlayerSettings.GetArchitecture(namedTarget);
string archName = arch switch { 0 => "x86_64", 1 => "arm64", 2 => "universal", _ => "unknown" };
return new { property, value = archName, raw = arch };
default:
return null;
}
}
public static string WriteProperty(string property, string value, NamedBuildTarget namedTarget)
{
try
{
switch (property.ToLowerInvariant())
{
case "product_name":
PlayerSettings.productName = value;
return null;
case "company_name":
PlayerSettings.companyName = value;
return null;
case "version":
PlayerSettings.bundleVersion = value;
return null;
case "bundle_id":
PlayerSettings.SetApplicationIdentifier(namedTarget, value);
return null;
case "scripting_backend":
var backendValue = value.ToLowerInvariant();
if (backendValue != "il2cpp" && backendValue != "mono")
return $"Unknown scripting_backend '{value}'. Valid: mono, il2cpp";
var impl = backendValue == "il2cpp"
? ScriptingImplementation.IL2CPP
: ScriptingImplementation.Mono2x;
PlayerSettings.SetScriptingBackend(namedTarget, impl);
return null;
case "defines":
PlayerSettings.SetScriptingDefineSymbols(namedTarget, value);
return null;
case "architecture":
int arch = value.ToLowerInvariant() switch
{
"x86_64" or "none" or "default" => 0,
"arm64" => 1,
"universal" => 2,
_ => -1
};
if (arch < 0)
return $"Unknown architecture '{value}'. Valid: x86_64, arm64, universal";
PlayerSettings.SetArchitecture(namedTarget, arch);
return null;
default:
return $"Unknown property '{property}'. Valid: product_name, company_name, version, bundle_id, scripting_backend, defines, architecture";
}
}
catch (Exception ex)
{
return $"Failed to set {property}: {ex.Message}";
}
}
public static readonly IReadOnlyList<string> ValidProperties = new[]
{
"product_name", "company_name", "version", "bundle_id",
"scripting_backend", "defines", "architecture"
};
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 51f7a2852819440eada31cda61039b6a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,165 @@
using System;
using UnityEditor;
using UnityEditor.Build;
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))
{
target = EditorUserBuildSettings.activeBuildTarget;
return true;
}
switch (name.ToLowerInvariant())
{
case "windows64": target = BuildTarget.StandaloneWindows64; return true;
case "windows": case "windows32": target = BuildTarget.StandaloneWindows; return true;
case "osx": case "macos": target = BuildTarget.StandaloneOSX; return true;
case "linux64": case "linux": target = BuildTarget.StandaloneLinux64; return true;
case "android": target = BuildTarget.Android; return true;
case "ios": target = BuildTarget.iOS; return true;
case "webgl": target = BuildTarget.WebGL; return true;
case "uwp": target = BuildTarget.WSAPlayer; return true;
case "tvos": target = BuildTarget.tvOS; return true;
default:
if (TryParseDefinedBuildTarget(name, out target))
{
return true;
}
target = default;
return false;
}
}
public static BuildTargetGroup GetTargetGroup(BuildTarget target)
{
switch (target)
{
case BuildTarget.StandaloneWindows:
case BuildTarget.StandaloneWindows64:
case BuildTarget.StandaloneOSX:
case BuildTarget.StandaloneLinux64:
return BuildTargetGroup.Standalone;
case BuildTarget.iOS: return BuildTargetGroup.iOS;
case BuildTarget.Android: return BuildTargetGroup.Android;
case BuildTarget.WebGL: return BuildTargetGroup.WebGL;
case BuildTarget.WSAPlayer: return BuildTargetGroup.WSA;
case BuildTarget.tvOS: return BuildTargetGroup.tvOS;
default:
if (IsVisionOSTarget(target)
&& Enum.TryParse(VisionOSName, true, out BuildTargetGroup visionOSGroup))
{
return visionOSGroup;
}
return BuildTargetGroup.Unknown;
}
}
public static NamedBuildTarget GetNamedBuildTarget(BuildTarget target)
{
return NamedBuildTarget.FromBuildTargetGroup(GetTargetGroup(target));
}
public static string TryResolveNamedBuildTarget(string name, out NamedBuildTarget namedTarget)
{
if (!TryResolveBuildTarget(name, out var buildTarget))
{
namedTarget = default;
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 = 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}";
switch (target)
{
case BuildTarget.StandaloneWindows:
case BuildTarget.StandaloneWindows64:
return $"{basePath}/{productName}.exe";
case BuildTarget.StandaloneOSX:
return $"{basePath}/{productName}.app";
case BuildTarget.StandaloneLinux64:
return $"{basePath}/{productName}.x86_64";
case BuildTarget.Android:
return EditorUserBuildSettings.buildAppBundle
? $"{basePath}/{productName}.aab"
: $"{basePath}/{productName}.apk";
case BuildTarget.iOS:
case BuildTarget.WebGL:
return $"{basePath}/{productName}";
default:
return $"{basePath}/{productName}";
}
}
public static int ResolveSubtarget(string subtarget)
{
if (string.IsNullOrEmpty(subtarget))
return (int)StandaloneBuildSubtarget.Player;
string lower = subtarget.ToLowerInvariant();
if (lower == "server")
return (int)StandaloneBuildSubtarget.Server;
return (int)StandaloneBuildSubtarget.Player;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c338ede07ac74e0ab9db99a1ac8c1a87
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: