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 { /// /// Read/inspect build configuration for agents (CLI-204): enumerate targets, read/write the mutable /// EditorUserBuildSettings fields, and list Build Profile assets. Scene-list management and /// target switching are intentionally elsewhere (add_scene_to_build/remove_scene_from_build /// from CLI-189, and switch_build_target). /// 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(); var seen = new HashSet(); 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() != 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 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); } /// Friendly label for common targets; falls back to the enum name. 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(); } } } /// /// Reflection shim over the Unity 6 Build Profile API (UnityEditor.Build.Profile.BuildProfile), /// so this assembly compiles and runs on older editors without the type. Used by /// list_build_profiles and by build's optional profileName activation. /// 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 List() { var list = new List(); 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)); } /// Activate the named profile via the API's static setter; returns false if unavailable. 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; } } }