using System; using System.Linq; using UnityEditor; using UnityEditor.Build.Reporting; using UnityEngine; namespace NightclubArcadia.EditorTools { /// /// Headless player builds. /// /// Not called "BuildPipeline" on purpose — that name collides with /// UnityEditor.BuildPipeline, which this has to call. /// /// The reason to have this at all rather than let the CLI drive Unity's built-in /// build: a flag-only build reports success even when it produced nothing useful, /// because Unity exits 0 unless the process itself failed. CI then goes green on a /// build that does not exist. Every entry point here inspects the BuildReport and /// calls EditorApplication.Exit(1) on anything short of success. /// /// unity build NightclubArcadia --target StandaloneOSX \ /// --execute-method NightclubArcadia.EditorTools.PlayerBuild.BuildMacOS \ /// -o build/NightclubArcadia.app /// /// The CLI forwards -o as -buildOutput, and honouring it is this method's job. /// public static class PlayerBuild { public static void BuildMacOS() => Build(BuildTarget.StandaloneOSX, "build/NightclubArcadia.app"); public static void BuildWindows() => Build(BuildTarget.StandaloneWindows64, "build/NightclubArcadia.exe"); public static void BuildLinux() => Build(BuildTarget.StandaloneLinux64, "build/NightclubArcadia"); static void Build(BuildTarget target, string defaultOutput) { var output = ArgValue("-buildOutput") ?? defaultOutput; var scenes = EditorBuildSettings.scenes .Where(s => s.enabled) .Select(s => s.path) .ToArray(); if (scenes.Length == 0) { Fail("No enabled scenes in the build settings — the player would have nothing to load."); return; } // Bootstrap loads the others by name at runtime, so it has to be index 0. if (!scenes[0].EndsWith("/Bootstrap.unity", StringComparison.Ordinal)) { Fail($"Scene 0 is '{scenes[0]}', expected Assets/Scenes/Bootstrap.unity. " + "The player opens scene 0 on launch."); return; } Debug.Log($"[PlayerBuild] {target} -> {output}"); foreach (var scene in scenes) { Debug.Log($"[PlayerBuild] scene: {scene}"); } var options = new BuildPlayerOptions { scenes = scenes, locationPathName = output, target = target, options = BuildOptions.None, }; BuildReport report; try { report = UnityEditor.BuildPipeline.BuildPlayer(options); } catch (Exception e) { Fail($"BuildPlayer threw: {e}"); return; } var summary = report.summary; Debug.Log($"[PlayerBuild] {summary.result} — {summary.totalSize} bytes in {summary.totalTime}, " + $"{summary.totalErrors} error(s), {summary.totalWarnings} warning(s)"); if (summary.result != BuildResult.Succeeded) { foreach (var step in report.steps) { foreach (var msg in step.messages.Where(m => m.type == LogType.Error || m.type == LogType.Exception)) { Debug.LogError($"[PlayerBuild] {step.name}: {msg.content}"); } } Fail($"Build did not succeed: {summary.result}"); return; } Debug.Log("[PlayerBuild] ok"); } static void Fail(string message) { Debug.LogError($"[PlayerBuild] {message}"); EditorApplication.Exit(1); } /// Reads "-name value" out of the command line Unity was launched with. static string ArgValue(string name) { var args = Environment.GetCommandLineArgs(); for (var i = 0; i < args.Length - 1; i++) { if (string.Equals(args[i], name, StringComparison.Ordinal)) { return args[i + 1]; } } return null; } } }