Add Unity Pipeline package support
This commit is contained in:
+337
@@ -0,0 +1,337 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Unity.Pipeline.Commands;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Build;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unity.Pipeline.Editor.Commands.Build
|
||||
{
|
||||
/// <summary>
|
||||
/// Read/inspect build configuration for agents (CLI-204): enumerate targets, read/write the mutable
|
||||
/// <c>EditorUserBuildSettings</c> fields, and list Build Profile assets. Scene-list management and
|
||||
/// target switching are intentionally elsewhere (<c>add_scene_to_build</c>/<c>remove_scene_from_build</c>
|
||||
/// from CLI-189, and <c>switch_build_target</c>).
|
||||
/// </summary>
|
||||
public static class BuildConfigCommands
|
||||
{
|
||||
[CliCommand("list_build_targets",
|
||||
"List the known BuildTarget values with their group and whether build support is installed.",
|
||||
MainThreadRequired = true)]
|
||||
public static object ListBuildTargets()
|
||||
{
|
||||
var targets = new List<BuildTargetInfo>();
|
||||
var seen = new HashSet<string>();
|
||||
|
||||
foreach (BuildTarget value in Enum.GetValues(typeof(BuildTarget)))
|
||||
{
|
||||
// Exclude NoTarget / internal sentinels (negative) and deprecated/obsolete values.
|
||||
if ((int)value <= 0)
|
||||
continue;
|
||||
|
||||
var name = value.ToString();
|
||||
if (!seen.Add(name))
|
||||
continue;
|
||||
|
||||
var field = typeof(BuildTarget).GetField(name);
|
||||
if (field != null && field.GetCustomAttribute<ObsoleteAttribute>() != null)
|
||||
continue;
|
||||
|
||||
BuildTargetGroup group;
|
||||
try { group = BuildPipeline.GetBuildTargetGroup(value); }
|
||||
catch { continue; }
|
||||
|
||||
bool installed;
|
||||
try { installed = BuildPipeline.IsBuildTargetSupported(group, value); }
|
||||
catch { installed = false; }
|
||||
|
||||
targets.Add(new BuildTargetInfo
|
||||
{
|
||||
Name = name,
|
||||
DisplayName = DisplayNameFor(value),
|
||||
TargetGroup = group.ToString(),
|
||||
IsInstalled = installed
|
||||
});
|
||||
}
|
||||
|
||||
return targets.OrderBy(t => t.TargetGroup).ThenBy(t => t.Name).ToList();
|
||||
}
|
||||
|
||||
[CliCommand("get_build_settings",
|
||||
"Read the current build configuration from EditorUserBuildSettings / EditorBuildSettings.",
|
||||
MainThreadRequired = true)]
|
||||
public static object GetBuildSettings()
|
||||
{
|
||||
var target = EditorUserBuildSettings.activeBuildTarget;
|
||||
var group = BuildPipeline.GetBuildTargetGroup(target);
|
||||
|
||||
return new BuildSettingsResult
|
||||
{
|
||||
ActiveBuildTarget = target.ToString(),
|
||||
ActiveBuildTargetGroup = group.ToString(),
|
||||
DevelopmentBuild = EditorUserBuildSettings.development,
|
||||
AllowDebugging = EditorUserBuildSettings.allowDebugging,
|
||||
ConnectWithProfiler = EditorUserBuildSettings.connectProfiler,
|
||||
BuildScriptsOnly = EditorUserBuildSettings.buildScriptsOnly,
|
||||
SymlinkSources = EditorUserBuildSettings.symlinkSources,
|
||||
Il2CppCodeGeneration = ReadIl2CppCodeGeneration(group),
|
||||
Scenes = EditorBuildSettings.scenes.Select(s => new BuildSceneEntry
|
||||
{
|
||||
Path = s.path,
|
||||
Guid = s.guid.ToString(),
|
||||
Enabled = s.enabled
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
[CliCommand("set_build_settings",
|
||||
"Set mutable EditorUserBuildSettings fields. Does NOT manage scenes (use add_scene_to_build / " +
|
||||
"remove_scene_from_build) or switch target (use switch_build_target). Use dry_run to preview.",
|
||||
MainThreadRequired = true)]
|
||||
public static object SetBuildSettings(
|
||||
[CliArg("settings", "Fields to change; omitted fields are left unchanged.")] SetBuildSettingsInput settings = null,
|
||||
[CliArg("confirm", "Apply the changes. Without it the call is refused.")] bool confirm = false,
|
||||
[CliArg("dry_run", "Preview the change without applying it.")] bool dryRun = false)
|
||||
{
|
||||
if (settings == null)
|
||||
return SetBuildSettingsResult.Fail("No 'settings' object provided.");
|
||||
|
||||
if (!dryRun && !confirm)
|
||||
return SetBuildSettingsResult.Fail(
|
||||
"Refused: this operation mutates the project. Re-run with confirm=true to apply, or dry_run=true to preview the change.");
|
||||
var result = new SetBuildSettingsResult { DryRun = dryRun, Success = true };
|
||||
var group = BuildPipeline.GetBuildTargetGroup(EditorUserBuildSettings.activeBuildTarget);
|
||||
|
||||
// For each supplied field: change it (recording in Applied) only if it differs from the
|
||||
// current value; otherwise record it in Skipped as a no-op.
|
||||
Plan(result, "developmentBuild", settings.DevelopmentBuild, EditorUserBuildSettings.development,
|
||||
v => EditorUserBuildSettings.development = v, dryRun);
|
||||
Plan(result, "allowDebugging", settings.AllowDebugging, EditorUserBuildSettings.allowDebugging,
|
||||
v => EditorUserBuildSettings.allowDebugging = v, dryRun);
|
||||
Plan(result, "connectWithProfiler", settings.ConnectWithProfiler, EditorUserBuildSettings.connectProfiler,
|
||||
v => EditorUserBuildSettings.connectProfiler = v, dryRun);
|
||||
Plan(result, "buildScriptsOnly", settings.BuildScriptsOnly, EditorUserBuildSettings.buildScriptsOnly,
|
||||
v => EditorUserBuildSettings.buildScriptsOnly = v, dryRun);
|
||||
Plan(result, "symlinkSources", settings.SymlinkSources, EditorUserBuildSettings.symlinkSources,
|
||||
v => EditorUserBuildSettings.symlinkSources = v, dryRun);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(settings.Il2CppCodeGeneration))
|
||||
{
|
||||
if (!TryParseIl2Cpp(settings.Il2CppCodeGeneration, out var requested))
|
||||
return SetBuildSettingsResult.Fail($"Invalid il2CppCodeGeneration '{settings.Il2CppCodeGeneration}'. Use OptimizeSpeed | OptimizeSize.");
|
||||
|
||||
var current = ReadIl2CppCodeGeneration(group);
|
||||
if (!string.Equals(current, requested.ToString(), StringComparison.Ordinal))
|
||||
{
|
||||
if (!dryRun)
|
||||
PlayerSettings.SetIl2CppCodeGeneration(NamedBuildTarget.FromBuildTargetGroup(group), requested);
|
||||
result.Applied["il2CppCodeGeneration"] = requested.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Skipped["il2CppCodeGeneration"] = current;
|
||||
}
|
||||
}
|
||||
|
||||
result.Message = dryRun
|
||||
? $"Dry run — {result.Applied.Count} field(s) would change, {result.Skipped.Count} unchanged."
|
||||
: $"Applied {result.Applied.Count} field(s); {result.Skipped.Count} unchanged.";
|
||||
return result;
|
||||
}
|
||||
|
||||
[CliCommand("list_build_profiles",
|
||||
"List Build Profile assets in the project (Unity 6 only). Returns feature_unavailable on earlier versions.",
|
||||
MainThreadRequired = true)]
|
||||
public static object ListBuildProfiles()
|
||||
{
|
||||
if (!BuildProfiles.IsSupported)
|
||||
return new { error = "Build Profiles require Unity 6 (6000.0) or later", code = "feature_unavailable" };
|
||||
|
||||
return BuildProfiles.List();
|
||||
}
|
||||
|
||||
// ---- Helpers ---------------------------------------------------------------------------
|
||||
|
||||
static void Plan(SetBuildSettingsResult result, string name, bool? requested, bool current,
|
||||
Action<bool> set, bool dryRun)
|
||||
{
|
||||
if (!requested.HasValue)
|
||||
return;
|
||||
|
||||
if (requested.Value != current)
|
||||
{
|
||||
if (!dryRun)
|
||||
set(requested.Value);
|
||||
result.Applied[name] = requested.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Skipped[name] = current;
|
||||
}
|
||||
}
|
||||
|
||||
static string ReadIl2CppCodeGeneration(BuildTargetGroup group)
|
||||
{
|
||||
try
|
||||
{
|
||||
return PlayerSettings.GetIl2CppCodeGeneration(NamedBuildTarget.FromBuildTargetGroup(group)).ToString();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Il2CppCodeGeneration.OptimizeSpeed.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
static bool TryParseIl2Cpp(string value, out Il2CppCodeGeneration parsed)
|
||||
{
|
||||
return Enum.TryParse(value.Trim(), ignoreCase: true, out parsed)
|
||||
&& Enum.IsDefined(typeof(Il2CppCodeGeneration), parsed);
|
||||
}
|
||||
|
||||
/// <summary>Friendly label for common targets; falls back to the enum name.</summary>
|
||||
static string DisplayNameFor(BuildTarget target)
|
||||
{
|
||||
switch (target)
|
||||
{
|
||||
case BuildTarget.StandaloneWindows: return "Windows (32-bit)";
|
||||
case BuildTarget.StandaloneWindows64: return "Windows 64-bit";
|
||||
case BuildTarget.StandaloneOSX: return "macOS";
|
||||
case BuildTarget.StandaloneLinux64: return "Linux 64-bit";
|
||||
case BuildTarget.Android: return "Android";
|
||||
case BuildTarget.iOS: return "iOS";
|
||||
case BuildTarget.tvOS: return "tvOS";
|
||||
case BuildTarget.WebGL: return "WebGL";
|
||||
case BuildTarget.WSAPlayer: return "Universal Windows Platform";
|
||||
case BuildTarget.PS4: return "PlayStation 4";
|
||||
case BuildTarget.PS5: return "PlayStation 5";
|
||||
case BuildTarget.XboxOne: return "Xbox One";
|
||||
case BuildTarget.Switch: return "Nintendo Switch";
|
||||
default: return target.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reflection shim over the Unity 6 Build Profile API (<c>UnityEditor.Build.Profile.BuildProfile</c>),
|
||||
/// so this assembly compiles and runs on older editors without the type. Used by
|
||||
/// <c>list_build_profiles</c> and by <c>build</c>'s optional <c>profileName</c> activation.
|
||||
/// </summary>
|
||||
internal static class BuildProfiles
|
||||
{
|
||||
const string TypeName = "UnityEditor.Build.Profile.BuildProfile, UnityEditor.CoreModule";
|
||||
|
||||
static readonly Type s_Type = ResolveType();
|
||||
|
||||
public static bool IsSupported => s_Type != null;
|
||||
|
||||
static Type ResolveType()
|
||||
{
|
||||
var t = Type.GetType(TypeName);
|
||||
if (t != null)
|
||||
return t;
|
||||
|
||||
// Fall back to a full-assembly scan — the assembly-qualified name can vary across versions.
|
||||
foreach (var asm in PipelineUtils.GetLoadedAssemblies())
|
||||
{
|
||||
t = asm.GetType("UnityEditor.Build.Profile.BuildProfile");
|
||||
if (t != null)
|
||||
return t;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static List<BuildProfileInfo> List()
|
||||
{
|
||||
var list = new List<BuildProfileInfo>();
|
||||
if (s_Type == null)
|
||||
return list;
|
||||
|
||||
var active = GetActive();
|
||||
foreach (var guid in AssetDatabase.FindAssets("t:BuildProfile"))
|
||||
{
|
||||
var path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
var asset = AssetDatabase.LoadAssetAtPath(path, s_Type);
|
||||
if (asset == null)
|
||||
continue;
|
||||
|
||||
list.Add(new BuildProfileInfo
|
||||
{
|
||||
Name = System.IO.Path.GetFileNameWithoutExtension(path),
|
||||
Guid = guid,
|
||||
Platform = ReadPlatform(asset),
|
||||
IsActive = active != null && ReferenceEquals(active, asset)
|
||||
});
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public static BuildProfileInfo FindByName(string name)
|
||||
{
|
||||
return List().FirstOrDefault(p => string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>Activate the named profile via the API's static setter; returns false if unavailable.</summary>
|
||||
public static bool TrySetActive(BuildProfileInfo info)
|
||||
{
|
||||
if (s_Type == null || info == null)
|
||||
return false;
|
||||
|
||||
var path = AssetDatabase.GUIDToAssetPath(info.Guid);
|
||||
var asset = AssetDatabase.LoadAssetAtPath(path, s_Type);
|
||||
if (asset == null)
|
||||
return false;
|
||||
|
||||
// Unity 6 exposes a static settable 'activeProfile' (and/or a SetActiveBuildProfile method).
|
||||
var prop = s_Type.GetProperty("activeProfile", BindingFlags.Public | BindingFlags.Static);
|
||||
if (prop != null && prop.CanWrite)
|
||||
{
|
||||
prop.SetValue(null, asset);
|
||||
return true;
|
||||
}
|
||||
|
||||
var method = s_Type.GetMethod("SetActiveBuildProfile", BindingFlags.Public | BindingFlags.Static);
|
||||
if (method != null)
|
||||
{
|
||||
method.Invoke(null, new[] { asset });
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static UnityEngine.Object GetActive()
|
||||
{
|
||||
try
|
||||
{
|
||||
var prop = s_Type.GetProperty("activeProfile", BindingFlags.Public | BindingFlags.Static);
|
||||
if (prop != null)
|
||||
return prop.GetValue(null) as UnityEngine.Object;
|
||||
|
||||
var method = s_Type.GetMethod("GetActiveBuildProfile", BindingFlags.Public | BindingFlags.Static);
|
||||
if (method != null)
|
||||
return method.Invoke(null, null) as UnityEngine.Object;
|
||||
}
|
||||
catch { /* best effort */ }
|
||||
return null;
|
||||
}
|
||||
|
||||
static string ReadPlatform(UnityEngine.Object asset)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Public property first, then the serialized backing field.
|
||||
var prop = s_Type.GetProperty("buildTarget", BindingFlags.Public | BindingFlags.Instance);
|
||||
if (prop != null)
|
||||
return prop.GetValue(asset)?.ToString() ?? string.Empty;
|
||||
|
||||
var field = s_Type.GetField("m_BuildTarget", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
if (field != null)
|
||||
return field.GetValue(asset)?.ToString() ?? string.Empty;
|
||||
}
|
||||
catch { /* best effort */ }
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user