Files
2026-07-13 12:49:17 +08:00

190 lines
6.4 KiB
C#

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);
}
}
}