using System; using System.IO; using System.Linq; using UnityEditor; using UnityEditor.Build.Reporting; using UnityEngine; namespace ES.EditorTools { /// /// ES Android 빌드 메뉴 (APK · AAB). /// 씬 목록 = EditorBuildSettings(활성 씬) · 버전·패키지명 = PlayerSettings 를 그대로 읽는다. /// CLI: unity build "" --target Android --execute-method ES.EditorTools.ESBuildMenu.BuildApkCli /// public static class ESBuildMenu { const string MenuRoot = "ES/Build/"; const string LogTag = "[ESBuild]"; const string OutputFolder = "Builds/Android"; [MenuItem(MenuRoot + "Android APK (Development)", priority = 1)] static void MenuApkDev() => Build(aab: false, development: true); [MenuItem(MenuRoot + "Android APK (Release)", priority = 2)] static void MenuApkRelease() => Build(aab: false, development: false); [MenuItem(MenuRoot + "Android AAB (Release)", priority = 3)] static void MenuAab() => Build(aab: true, development: false); [MenuItem(MenuRoot + "Open Build Folder", priority = 20)] static void MenuOpenFolder() { string dir = GetOutputDir(); Directory.CreateDirectory(dir); EditorUtility.RevealInFinder(dir); } [MenuItem(MenuRoot + "Android APK (Development)", true)] [MenuItem(MenuRoot + "Android APK (Release)", true)] [MenuItem(MenuRoot + "Android AAB (Release)", true)] static bool ValidateBuild() => !EditorApplication.isPlayingOrWillChangePlaymode && !EditorApplication.isCompiling; // CLI 진입점 (batchmode) — 실패 시 종료 코드 1 public static void BuildApkCli() => ExitWith(Build(aab: false, development: HasArg("-esDevelopment"))); public static void BuildAabCli() => ExitWith(Build(aab: true, development: false)); public static bool Build(bool aab, bool development) => Build(aab, development, !Application.isBatchMode); /// false = 완료 대화상자 생략 (CLI eval 로 GUI 에디터에서 호출할 때) public static bool Build(bool aab, bool development, bool interactive) { if (EditorApplication.isPlayingOrWillChangePlaymode) { Debug.LogError($"{LogTag} Play 중에는 빌드할 수 없습니다."); return false; } string[] scenes = EditorBuildSettings.scenes.Where(s => s.enabled).Select(s => s.path).ToArray(); if (scenes.Length == 0) { Debug.LogError($"{LogTag} Build Settings 에 활성 씬이 없습니다."); return false; } if (EditorUserBuildSettings.activeBuildTarget != BuildTarget.Android) { Debug.Log($"{LogTag} 빌드 타깃을 Android 로 전환합니다."); if (!EditorUserBuildSettings.SwitchActiveBuildTarget(BuildTargetGroup.Android, BuildTarget.Android)) { Debug.LogError($"{LogTag} Android 타깃 전환 실패 (Android 모듈 설치 확인)."); return false; } } if (!PlayerSettings.Android.useCustomKeystore) Debug.LogWarning($"{LogTag} 사용자 키스토어 미설정 — 디버그 키로 서명됩니다 (스토어 업로드 불가)."); string dir = GetOutputDir(); Directory.CreateDirectory(dir); string fileName = string.Format("{0}_{1}_{2}{3}_{4}.{5}", Sanitize(PlayerSettings.productName), PlayerSettings.bundleVersion, PlayerSettings.Android.bundleVersionCode, development ? "_dev" : "", DateTime.Now.ToString("yyyyMMdd_HHmm"), aab ? "aab" : "apk"); string outPath = Path.Combine(dir, fileName); bool prevAab = EditorUserBuildSettings.buildAppBundle; EditorUserBuildSettings.buildAppBundle = aab; try { var options = new BuildPlayerOptions { scenes = scenes, locationPathName = outPath, target = BuildTarget.Android, targetGroup = BuildTargetGroup.Android, options = development ? BuildOptions.Development : BuildOptions.None, }; Debug.Log($"{LogTag} 시작 — {(aab ? "AAB" : "APK")} · 씬 {scenes.Length}개 ({string.Join(", ", scenes.Select(Path.GetFileNameWithoutExtension))}) · {outPath}"); BuildReport report = BuildPipeline.BuildPlayer(options); BuildSummary s = report.summary; bool ok = s.result == BuildResult.Succeeded; string msg = ok ? $"{LogTag} 성공 — {outPath} · {new FileInfo(outPath).Length / (1024f * 1024f):F1} MB · {s.totalTime.TotalSeconds:F0} s" : $"{LogTag} 실패 — result={s.result} · errors={s.totalErrors}"; if (ok) Debug.Log(msg); else Debug.LogError(msg); if (interactive) { if (ok && EditorUtility.DisplayDialog("ES Build", msg, "폴더 열기", "닫기")) EditorUtility.RevealInFinder(outPath); else if (!ok) EditorUtility.DisplayDialog("ES Build", msg + "\nConsole 을 확인하세요.", "확인"); } return ok; } finally { EditorUserBuildSettings.buildAppBundle = prevAab; } } static string GetOutputDir() => Path.Combine(Directory.GetParent(Application.dataPath).FullName, OutputFolder); static string Sanitize(string name) { foreach (char c in Path.GetInvalidFileNameChars()) name = name.Replace(c, '_'); return name.Replace(' ', '_'); } static bool HasArg(string arg) => Environment.GetCommandLineArgs().Contains(arg); static void ExitWith(bool ok) { if (Application.isBatchMode) EditorApplication.Exit(ok ? 0 : 1); } } }