// DO WHAT THE CRAP YOU WANT TO PUBLIC LICENSE // Version 3, December 2018 // // Copyright (C) 2018 Daniel Christmas & Sam Hocevar // // Everyone is permitted to copy and distribute verbatim or modified // copies of this license document, and changing it is allowed as long // as the name is changed. // // DO WHAT THE CRAP YOU WANT TO PUBLIC LICENSE // TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION // // 0. You just DO WHAT THE CRAP YOU WANT TO. #define FMOD // comment if you're not using fmod using UnityEditor; using UnityEngine; using System.IO; using System; using UnityEditor.Build.Reporting; class BatchBuilderSettings : ScriptableObject { [Serializable] public struct Settings { public string Name; public bool Active; public BuildTarget Target; public BuildTargetGroup Group; public string Symbols; public string Path; public string Filename; public string Extension { get { switch (Target) { case BuildTarget.StandaloneOSX: return ".app"; case BuildTarget.StandaloneWindows: case BuildTarget.StandaloneWindows64: return ".exe"; default: return ".bin"; } } } public UnityEditor.AnimatedValues.AnimBool Editing; } public Settings[] Builds = new Settings[0]; [Serializable] public struct Symbol { public bool Active; public string Name; public string DisplayName { get { return this.Name.ToUpper().Replace(" ","_"); } } public Symbol(bool _active, string _name) { this.Active = _active; this.Name = _name.ToUpper().Replace(" ", "_"); } } public Symbol[] GeneralSymbols; public bool Debug = false; public bool LZ4Compression = true; } class BatchBuilder : EditorWindow { const string settingsPath = "Assets/Editor/Resources/BatchBuild.asset"; BatchBuilderSettings settings; Vector2 scrollTargets; Vector2 scrollSymbols; enum Step { SetSymbols, #if FMOD AudioCopy, #endif Compilation, Build } int building = -1; int succeeded = 0; int failed = 0; Step step = Step.SetSymbols; System.Diagnostics.Stopwatch timer = new System.Diagnostics.Stopwatch(); [MenuItem("Tools/Batch Builder")] static void Open() { EditorWindow w = GetWindow(); w.minSize = new Vector2(750, 320); } void OnEnable() { settings = AssetDatabase.LoadAssetAtPath(settingsPath); if (settings == null) { settings = CreateInstance(); AssetDatabase.CreateAsset(settings, settingsPath); AssetDatabase.SaveAssets(); } EditorApplication.update += Update; foreach (BatchBuilderSettings.Settings s in settings.Builds) s.Editing.valueChanged.AddListener(Repaint); } void OnDisable() { EditorApplication.update -= Update; } void OnGUI() { GUI.enabled = building == -1; GUILayout.Label("Targets", EditorStyles.boldLabel); GUILayout.BeginHorizontal(); { GUILayout.Label("Name", EditorStyles.boldLabel, GUILayout.Width(200)); GUILayout.FlexibleSpace(); GUILayout.Label("Directory", EditorStyles.boldLabel, GUILayout.Width(200)); GUILayout.Label("Filename", EditorStyles.boldLabel, GUILayout.Width(200)); GUILayout.Space(60); // width of edit, X } GUILayout.EndHorizontal(); EditorGUI.BeginChangeCheck(); { Undo.RecordObject(settings, "Edit Batch Build Settings"); BatchBuilderSettings.Settings[] builds = settings.Builds; scrollTargets = EditorGUILayout.BeginScrollView(scrollTargets); { for (int i = 0; i < builds.Length; i++) { bool deleted = false; var oneLevelUp = Application.dataPath + "/../" + builds[i].Path; DirectoryInfo dirInfo = new DirectoryInfo(oneLevelUp); string currentBuildPath = dirInfo.FullName + "\\" + builds[i].Filename + builds[i].Extension; GUILayout.BeginHorizontal(); { if (builds[i].Editing.target) { builds[i].Active = EditorGUILayout.Toggle(builds[i].Active, GUILayout.Width(15)); builds[i].Name = EditorGUILayout.TextField(builds[i].Name); GUILayout.FlexibleSpace(); builds[i].Path = EditorGUILayout.TextField(builds[i].Path, GUILayout.Width(200)); builds[i].Filename = EditorGUILayout.TextField(builds[i].Filename, GUILayout.Width(200)); } else { builds[i].Active = EditorGUILayout.ToggleLeft(builds[i].Name, builds[i].Active); GUILayout.FlexibleSpace(); GUILayout.Label(builds[i].Path, GUILayout.Width(200)); GUILayout.Label(builds[i].Filename, GUILayout.Width(200)); } bool wasEditing = builds[i].Editing.target; builds[i].Editing.target = GUILayout.Toggle(builds[i].Editing.target, "Edit", "Button"); if (wasEditing && !builds[i].Editing.target) GUI.FocusControl(string.Empty); if (File.Exists(currentBuildPath)) { #if UNITY_EDITOR_WIN if (GUILayout.Button("Show in Explorer")) #elif UNITY_EDITOR_OSX if (GUILayout.Button("Reveal in Finder")) #else if (GUILayout.Button("Show")) #endif { EditorUtility.RevealInFinder(currentBuildPath);//show in explorer } if (GUILayout.Button("Open")) { Application.OpenURL(currentBuildPath);//open build } } if (GUILayout.Button("X", GUILayout.Width(20))) { deleted = true; ArrayUtility.RemoveAt(ref settings.Builds, i); GUI.FocusControl(string.Empty); i--; } } GUILayout.EndHorizontal(); if (!deleted && builds[i].Editing.target) { EditorGUILayout.BeginFadeGroup(builds[i].Editing.faded); { builds[i].Target = (BuildTarget)EditorGUILayout.EnumPopup("Target", builds[i].Target); builds[i].Group = (BuildTargetGroup)EditorGUILayout.EnumPopup("Group", builds[i].Group); builds[i].Symbols = EditorGUILayout.TextField("Symbols", builds[i].Symbols); } EditorGUILayout.EndFadeGroup(); } } } EditorGUILayout.EndScrollView(); GUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); if (GUILayout.Button("Add Build")) { ArrayUtility.Insert(ref settings.Builds, settings.Builds.Length, new BatchBuilderSettings.Settings() { Name = "New Build", Path = "Builds/New", Filename = PlayerSettings.productName, Target = BuildTarget.StandaloneWindows64, Group = BuildTargetGroup.Standalone, Symbols = "TMPPRESENT", Active = false, Editing = new UnityEditor.AnimatedValues.AnimBool(true), }); } GUILayout.EndHorizontal(); GUILayout.Label("General Symbols", EditorStyles.boldLabel); scrollSymbols = EditorGUILayout.BeginScrollView(scrollSymbols); { for (int i = 0; i < settings.GeneralSymbols.Length; i++) { GUILayout.BeginHorizontal(); { settings.GeneralSymbols[i].Active = EditorGUILayout.Toggle(settings.GeneralSymbols[i].Active, GUILayout.Width(15)); settings.GeneralSymbols[i].Name = GUILayout.TextField(settings.GeneralSymbols[i].DisplayName); if (GUILayout.Button("X", GUILayout.Width(20))) { ArrayUtility.RemoveAt(ref settings.GeneralSymbols, i); i--; } } GUILayout.EndHorizontal(); } } EditorGUILayout.EndScrollView(); GUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); if (GUILayout.Button("Add Symbol")) { BatchBuilderSettings.Symbol newSymbol = new BatchBuilderSettings.Symbol(false, "New Symbol"); ArrayUtility.Insert(ref settings.GeneralSymbols, settings.GeneralSymbols.Length, newSymbol); } GUILayout.EndHorizontal(); GUILayout.Label("Options", EditorStyles.boldLabel); GUILayout.BeginHorizontal(); { settings.Debug = EditorGUILayout.ToggleLeft("Debug", settings.Debug); settings.LZ4Compression = EditorGUILayout.ToggleLeft("LZ4 Compression", settings.LZ4Compression); GUILayout.FlexibleSpace(); } GUILayout.EndHorizontal(); } if (EditorGUI.EndChangeCheck()) EditorUtility.SetDirty(settings); if (building == -1) { if (GUILayout.Button("Build")) { timer.Start(); building = 0; succeeded = 0; failed = 0; step = Step.SetSymbols; } } else { GUI.enabled = true; Rect rect = EditorGUILayout.GetControlRect(false, 20); int totalSteps = Enum.GetNames(typeof(Step)).Length; float progress = (building * totalSteps + (int)step) / (float)(settings.Builds.Length * totalSteps); string text = string.Format("{0}: {1}", settings.Builds[building].Name, step); EditorGUI.ProgressBar(rect, progress, text); } } private void Update() { if (building >= 0) { Repaint(); if (settings.Builds[building].Active) { switch (step) { case Step.SetSymbols: { SetSymbols(settings.Builds[building]); step = Step.Compilation; break; } case Step.Compilation: { if (!EditorApplication.isCompiling) step = Step.Build; break; } case Step.Build: { if (Build(settings.Builds[building])) { building = -1; Debug.LogWarning("Builds canceled"); } else { step = Step.SetSymbols; if (building != -1) building++; } break; } } } else { building++; } if (building >= settings.Builds.Length) { building = -1; timer.Stop(); Debug.LogFormat("Builds complete: {0} succeeded, {1} failed ({2})", succeeded, failed, timer.Elapsed); if (succeeded > 0) { BatchBuilderSettings.Settings currentBuild = settings.Builds[0]; string currentBuildPath = currentBuild.Path + "/" + currentBuild.Filename; //Debug.Log(currentBuildPath); //EditorUtility.RevealInFinder(currentBuildPath);//show in explorer EditorApplication.Beep(); } } } } void SetSymbols(BatchBuilderSettings.Settings build) { string extraSymbols = ""; foreach (BatchBuilderSettings.Symbol symbol in settings.GeneralSymbols) if (symbol.Active) extraSymbols += ";" + symbol.Name; PlayerSettings.SetScriptingDefineSymbolsForGroup(build.Group, build.Symbols + extraSymbols); } bool Build(BatchBuilderSettings.Settings build) { if (Directory.Exists(build.Path)) Directory.Delete(build.Path, true); Directory.CreateDirectory(build.Path); string file = Path.Combine(build.Path, build.Filename); BuildOptions options = BuildOptions.StrictMode; if (settings.Debug) options |= BuildOptions.Development | BuildOptions.ConnectWithProfiler; else if (settings.LZ4Compression) options |= BuildOptions.CompressWithLz4; Debug.LogFormat("Starting build {0} ({1})", build.Name, PlayerSettings.GetScriptingDefineSymbolsForGroup(build.Group)); bool success = false, canceled = false; try { BuildReport buildReport = BuildPipeline.BuildPlayer(EditorBuildSettings.scenes, file + build.Extension, build.Target, options); BuildSummary buildSummary = buildReport.summary; BuildResult result = buildSummary.result; if (result == BuildResult.Succeeded) success = true; if (!success) { if (result == BuildResult.Cancelled) canceled = true; else Debug.LogError("Error while building " + build.Name); } } catch (Exception ex) { Debug.LogError("Error while building " + build.Name + ": " + ex.Message); } if (success) succeeded++; else failed++; return canceled; } }