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