using System; using System.Collections.Generic; using System.IO; using System.Linq; using UnityEditor; using UnityEditor.AddressableAssets; using UnityEditor.AddressableAssets.Settings; using UnityEditor.Build; using UnityEditor.Build.Reporting; using UnityEditor.Callbacks; #if UNITY_IOS using UnityEditor.iOS.Xcode; using UnityEditor.iOS.Xcode.Extensions; #endif using UnityEngine; /// /// WL(What's your level?) 자동 빌드 메뉴 — 수상한 잡화점 AutoBuild 구조를 WL 에 맞게 이식 (2026-10-11 #836 · PD 지시). /// /// 메뉴 (Unity 상단 AutoBuild/) /// · Build APK with KeyStore = 테스트 APK. bundleVersion 은 현재값(9.9.9 = dev PlayFab · TitleInfo 분기) 유지 · FGB_LIVE 기본 OFF /// · Build Test AAB = 테스트 AAB(Play 내부 테스트 트랙용). 버전·심볼은 다이얼로그 /// · Build Live AAB = 출시 AAB. 버전·버전코드 다이얼로그(기본 = 마지막+1) · FGB_LIVE 기본 ON · 네이티브 symbols.zip 생성 /// · Addressables Build Only = 플레이어 빌드 없이 Addressables 콘텐츠만 빌드(WL_Local 프로필) /// · Set_AndroidKeyStore / Open Build Folder /// /// 공통 절차 = 키스토어 지정 → 심볼(FGB_LIVE 만 관리 · 기존 UNITY_PIPELINE_URP 등 보존) → VersionCode.txt → Addressables 콘텐츠 빌드 → BuildPlayer → 심볼·프로필 복원. /// Addressables: 활성 프로필은 CCD(이전 개발자 버킷 · 그룹 20 개가 Remote 경로)라 그대로 빌드하면 기기에서 원격 로드가 실패한다. /// → 빌드 시에만 「WL_Local」 프로필(Remote.* = Local.* 경로 · 없으면 자동 생성)로 바꿔 전 그룹을 APK/AAB 안(StreamingAssets)에 넣고, 끝나면 원래 프로필로 복원. /// 버전 권위 = Assets/Editor/BuildVersionBackup.txt (잡화점과 동일 · ProjectSettings 가 다른 커밋으로 덮여도 마지막 빌드값 복원). /// CLI(대화 상자 없이) = AutoBuild.Build_TestAPK_NoDialog() · AutoBuild.Build_LiveAAB_NoDialog("1.0.0", 3) /// public static class AutoBuild { // ── 산출물 경로 (프로젝트 루트 기준 · AndroidData/ 는 git 추적, *.apk·*.aab 는 .gitignore) ── const string OutDir = "AndroidData"; const string PathTestApk = OutDir + "/WL_Test_"; const string PathTestAab = OutDir + "/WL_TestAAB_"; const string PathLiveAab = OutDir + "/WL_Live_"; const string VersionCodeTxt = "Assets/Resources/VersionCode.txt"; // 런타임 ServerInfo.VersionCode (TitleInfo 가 읽음) const string BuildVersionBackupPath = "Assets/Editor/BuildVersionBackup.txt"; // 마지막 빌드 버전/버전코드 권위(팀 공유) const string AddrLocalProfile = "WL_Local"; // 이 클래스가 관리하는 심볼만 넣고 뺀다. 나머지(UNITY_PIPELINE_URP·MOREMOUNTAINS_* 등)는 그대로 보존. const string SymLive = "FGB_LIVE"; static int s_ManualVersionCode = -1; // 다이얼로그 입력 버전코드 (-1 = 미입력 → 현재값 유지) static bool s_Live; // FGB_LIVE 주입 여부 static bool s_DevBuild; // BuildOptions.Development static string s_SavedDefines; // 빌드 전 Scripting Define 원문 (복원용) static string s_SavedAddrProfile; // 빌드 전 Addressables 활성 프로필 id (복원용) static string s_SavedBundleVersion; // 빌드 전 bundleVersion (복원용) // ───────────────────────── 버전 백업 (잡화점 동일) ───────────────────────── static void Load_VersionBackup(ref string ver, ref int code) { if (!File.Exists(BuildVersionBackupPath)) return; foreach (var raw in File.ReadAllLines(BuildVersionBackupPath)) { var line = raw.Trim(); if (line.StartsWith("version=")) { var v = line.Substring("version=".Length).Trim(); if (!string.IsNullOrEmpty(v)) ver = v; } else if (line.StartsWith("versionCode=") && int.TryParse(line.Substring("versionCode=".Length).Trim(), out var c)) code = c; } } static void Save_VersionBackup(string ver, int code) { File.WriteAllText(BuildVersionBackupPath, $"version={ver}\nversionCode={code}\n"); AssetDatabase.ImportAsset(BuildVersionBackupPath); } /// 마지막 빌드 버전/코드 — 백업 txt 가 권위. 백업이 없을 때(최초 1회)만 PlayerSettings. static void Get_LastVersion(out string ver, out int code) { ver = PlayerSettings.bundleVersion; code = PlayerSettings.Android.bundleVersionCode; Load_VersionBackup(ref ver, ref code); } static void Apply_ManualVersionCode_And_Backup() { if (s_ManualVersionCode < 0) return; PlayerSettings.Android.bundleVersionCode = s_ManualVersionCode; Save_VersionBackup(PlayerSettings.bundleVersion, s_ManualVersionCode); s_ManualVersionCode = -1; } // ───────────────────────── 공통 ───────────────────────── [MenuItem("AutoBuild/Set_AndroidKeyStore")] static void Set_AndroidKeyStore() { PlayerSettings.Android.useCustomKeystore = true; PlayerSettings.Android.keystoreName = OutDir + "/himminji.keystore"; PlayerSettings.Android.keyaliasName = "nerdnavis"; PlayerSettings.Android.keystorePass = PlayerSettings.Android.keyaliasPass = "himminji3#"; } /// FGB_LIVE 만 넣고/빼고 나머지 심볼은 보존. 원문은 s_SavedDefines 에 보관 → Common_End 에서 복원. static void Set_LiveSymbol(bool live) { var nbt = NamedBuildTarget.Android; s_SavedDefines = PlayerSettings.GetScriptingDefineSymbols(nbt); var list = s_SavedDefines.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).Where(s => s != SymLive).ToList(); if (live) list.Add(SymLive); PlayerSettings.SetScriptingDefineSymbols(nbt, string.Join(";", list)); } static void Restore_Symbols() { if (s_SavedDefines == null) return; PlayerSettings.SetScriptingDefineSymbols(NamedBuildTarget.Android, s_SavedDefines); s_SavedDefines = null; } static void Common() { EditorUserBuildSettings.SwitchActiveBuildTarget(BuildTargetGroup.Android, BuildTarget.Android); PlayerSettings.Android.splitApplicationBinary = false; PlayerSettings.Android.targetArchitectures = AndroidArchitecture.ARMv7 | AndroidArchitecture.ARM64; Set_AndroidKeyStore(); Set_LiveSymbol(s_Live); Apply_ManualVersionCode_And_Backup(); File.WriteAllText(VersionCodeTxt, PlayerSettings.Android.bundleVersionCode.ToString()); AssetDatabase.ImportAsset(VersionCodeTxt); } /// 빌드 버전 지정. 원래 값은 s_SavedBundleVersion 에 보관 → Common_End 에서 복원 (에디터는 9.9.9 = dev 서버 유지 · 1.0.0 등이 남으면 Play 시 업데이트 팝업). static void Set_BuildVersion(string ver) { s_SavedBundleVersion = PlayerSettings.bundleVersion; PlayerSettings.bundleVersion = ver; } static void Restore_BuildVersion() { if (s_SavedBundleVersion == null) return; PlayerSettings.bundleVersion = s_SavedBundleVersion; s_SavedBundleVersion = null; } static void Common_End() { Restore_BuildVersion(); Restore_Symbols(); Restore_AddressablesProfile(); AssetDatabase.SaveAssets(); } /// 네이티브 디버그 심볼(symbols.zip) — 라이브 AAB 만 ON (Play Console·Cloud Diagnostics 역심볼화). IL2CPP 스택은 파일·라인까지 상시. static void Set_AndroidSymbols(bool on) { UnityEditor.Android.UserBuildSettings.DebugSymbols.level = on ? Unity.Android.Types.DebugSymbolLevel.Full : Unity.Android.Types.DebugSymbolLevel.None; UnityEditor.Android.UserBuildSettings.DebugSymbols.format = Unity.Android.Types.DebugSymbolFormat.Zip; PlayerSettings.SetIl2CppStacktraceInformation(NamedBuildTarget.Android, Il2CppStacktraceInformation.MethodFileLineNumber); } // ───────────────────────── Addressables ───────────────────────── /// WL_Local 프로필(Remote.* 를 Local.* 경로로) 활성화 → 전 그룹이 StreamingAssets 로 들어간다. 없으면 Default 복제로 생성. static bool Activate_AddressablesLocalProfile() { var settings = AddressableAssetSettingsDefaultObject.Settings; if (settings == null) { Debug.LogError("[AutoBuild] AddressableAssetSettings 없음"); return false; } var ps = settings.profileSettings; string id = ps.GetProfileId(AddrLocalProfile); if (string.IsNullOrEmpty(id)) { id = ps.AddProfile(AddrLocalProfile, ps.GetProfileId("Default")); ps.SetValue(id, AddressableAssetSettings.kRemoteBuildPath, ps.GetValueByName(id, AddressableAssetSettings.kLocalBuildPath)); ps.SetValue(id, AddressableAssetSettings.kRemoteLoadPath, ps.GetValueByName(id, AddressableAssetSettings.kLocalLoadPath)); EditorUtility.SetDirty(settings); Debug.Log($"[AutoBuild] Addressables 프로필 '{AddrLocalProfile}' 생성 (Remote = Local 경로)"); } s_SavedAddrProfile = settings.activeProfileId; settings.activeProfileId = id; return true; } static void Restore_AddressablesProfile() { if (string.IsNullOrEmpty(s_SavedAddrProfile)) return; var settings = AddressableAssetSettingsDefaultObject.Settings; if (settings != null) settings.activeProfileId = s_SavedAddrProfile; s_SavedAddrProfile = null; } /// 앱 빌드 직전 Addressables 콘텐츠 빌드(WL_Local). SBP 캐시 비우고 풀빌드 → stale 번들 차단. 실패 시 false. static bool Build_Addressables_BeforePlayer() { if (!Activate_AddressablesLocalProfile()) return false; UnityEditor.Build.Pipeline.Utilities.BuildCache.PurgeCache(false); AddressableAssetSettings.BuildPlayerContent(out var result); if (result != null && !string.IsNullOrEmpty(result.Error)) { Debug.LogError($"[AutoBuild] Addressables 콘텐츠 빌드 실패 — 앱 빌드 중단: {result.Error}"); return false; } Debug.Log($"[AutoBuild] Addressables 콘텐츠 빌드 완료 ({AddrLocalProfile} · {result?.Duration:F0}s · {result?.OutputPath})"); return true; } [MenuItem("AutoBuild/Addressables Build Only (WL_Local)")] static void Build_AddressablesOnly() { try { Build_Addressables_BeforePlayer(); } finally { Restore_AddressablesProfile(); AssetDatabase.SaveAssets(); } } // ───────────────────────── 플레이어 빌드 ───────────────────────── static string[] EnabledScenes() => EditorBuildSettings.scenes.Where(s => s.enabled).Select(s => s.path).ToArray(); static bool BuildStart(string path, bool appBundle) { Directory.CreateDirectory(OutDir); EditorUserBuildSettings.buildAppBundle = appBundle; var opts = new BuildPlayerOptions { scenes = EnabledScenes(), locationPathName = path, target = BuildTarget.Android, targetGroup = BuildTargetGroup.Android, options = s_DevBuild ? (BuildOptions.Development | BuildOptions.AllowDebugging) : BuildOptions.None, }; var report = BuildPipeline.BuildPlayer(opts); var s = report.summary; if (s.result == BuildResult.Succeeded) { Debug.Log($"[AutoBuild] 빌드 성공 {path} · {s.totalSize / 1048576f:F1} MB · {s.totalTime.TotalMinutes:F1} 분 · 경고 {s.totalWarnings}"); if (!Application.isBatchMode) Show_BuildFolder(); return true; } Debug.LogError($"[AutoBuild] 빌드 실패 {path} · result={s.result} · errors={s.totalErrors}"); return false; } [MenuItem("AutoBuild/Open Build Folder")] static void Show_BuildFolder() { Directory.CreateDirectory(OutDir); System.Diagnostics.Process.Start(Path.GetFullPath(OutDir)); } static string OutName(string prefix, string ext) => $"{prefix}{PlayerSettings.bundleVersion}_({PlayerSettings.Android.bundleVersionCode}){ext}"; /// 빌드 파이프라인 1회 실행. 다이얼로그 뒤 호출 — s_Live/s_DevBuild/s_ManualVersionCode 가 이미 정해진 상태. static bool Run(string prefix, string ext, bool appBundle, bool symbols) { try { Common(); Set_AndroidSymbols(symbols); if (!Build_Addressables_BeforePlayer()) return false; return BuildStart(OutName(prefix, ext), appBundle); } finally { Common_End(); } } // ── 테스트 APK: bundleVersion 은 그대로(9.9.9 = dev 서버) · 버전코드 기본 = 마지막+1 ── [MenuItem("AutoBuild/Build APK with KeyStore")] static void Build_APK_with_KeyStore() { Get_LastVersion(out string ver, out int code); code += 1; if (!Application.isBatchMode) { if (!BuildVersionDialog.Show($"Test APK 빌드 (마지막 v{ver} / code {code - 1}).\n9.9.9 = dev PlayFab(TitleInfo 분기) · versionCode 기본 = 마지막+1.", ref ver, ref code, live: false, out s_Live, out s_DevBuild)) return; } else { s_Live = false; s_DevBuild = false; } Set_BuildVersion(ver); s_ManualVersionCode = code; Run(PathTestApk, ".apk", appBundle: false, symbols: false); } // ── 테스트 AAB: Play 내부 테스트 트랙용 (서명·버전은 라이브와 같은 흐름 · symbols 생성) ── [MenuItem("AutoBuild/Build Test AAB")] static void Build_Test_AAB() { Get_LastVersion(out string ver, out int code); code += 1; if (!Application.isBatchMode) { if (!BuildVersionDialog.Show($"Test AAB 빌드 (마지막 v{ver} / code {code - 1}).\nPlay 내부 테스트 트랙용 · versionCode 기본 = 마지막+1.", ref ver, ref code, live: false, out s_Live, out s_DevBuild)) return; } else { s_Live = false; s_DevBuild = false; } Set_BuildVersion(ver); s_ManualVersionCode = code; Run(PathTestAab, ".aab", appBundle: true, symbols: true); } // ── 라이브 AAB: 출시. 버전은 9.9.9 가 아니어야 라이브 서버(TitleInfo) · FGB_LIVE 기본 ON ── [MenuItem("AutoBuild/Build Live AAB")] static void Build_AAB() { Get_LastVersion(out string ver, out int code); code += 1; if (!Application.isBatchMode) { if (!BuildVersionDialog.Show($"Live AAB 빌드 (마지막 v{ver} / code {code - 1}).\n출시 버전은 9.9.9 가 아니어야 라이브 서버로 붙습니다 · versionCode 기본 = 마지막+1.", ref ver, ref code, live: true, out s_Live, out s_DevBuild)) return; } else { s_Live = true; s_DevBuild = false; } if (ver == "9.9.9" && !Application.isBatchMode && !EditorUtility.DisplayDialog("버전 확인", "bundleVersion 이 9.9.9(dev 서버)입니다. 출시 AAB 로 계속할까요?", "계속", "중단")) return; Set_BuildVersion(ver); s_ManualVersionCode = code; Run(PathLiveAab, ".aab", appBundle: true, symbols: true); } // ───────────────────────── CLI 진입점 (대화 상자 없음 · unity command eval 에서 호출) ───────────────────────── public static bool Build_TestAPK_NoDialog(bool devBuild = false) { Get_LastVersion(out string ver, out int code); s_Live = false; s_DevBuild = devBuild; Set_BuildVersion(ver); s_ManualVersionCode = code + 1; return Run(PathTestApk, ".apk", appBundle: false, symbols: false); } public static bool Build_LiveAAB_NoDialog(string version, int versionCode) { s_Live = true; s_DevBuild = false; Set_BuildVersion(version); s_ManualVersionCode = versionCode; return Run(PathLiveAab, ".aab", appBundle: true, symbols: true); } #if UNITY_IOS // iOS 는 론칭 단계에서 PD 지시로 정비 (Facebook SDK 참조 제거 · 최소 유지) [MenuItem("AutoBuild/IOS Xcode")] static void Build_IOS_Xcode() { PlayerSettings.iOS.buildNumber = (int.Parse(PlayerSettings.iOS.buildNumber) + 1).ToString(); File.WriteAllText(VersionCodeTxt, PlayerSettings.iOS.buildNumber); PlayerSettings.iOS.appleEnableAutomaticSigning = true; PlayerSettings.iOS.sdkVersion = iOSSdkVersion.DeviceSDK; EditorUserBuildSettings.SwitchActiveBuildTarget(BuildTargetGroup.iOS, BuildTarget.iOS); var opts = new BuildPlayerOptions { scenes = EnabledScenes(), locationPathName = "IOSData/IOSXcode_" + PlayerSettings.bundleVersion + "_" + PlayerSettings.iOS.buildNumber, target = BuildTarget.iOS, targetGroup = BuildTargetGroup.iOS }; BuildPipeline.BuildPlayer(opts); } [PostProcessBuild(1)] static void OnPostProcessBuild(BuildTarget buildTarget, string path) { if (buildTarget != BuildTarget.iOS) return; var plistPath = Path.Combine(path, "Info.plist"); var plist = new PlistDocument(); plist.ReadFromFile(plistPath); plist.root.SetBoolean("ITSAppUsesNonExemptEncryption", false); plist.WriteToFile(plistPath); string pbxPath = PBXProject.GetPBXProjectPath(path); var proj = new PBXProject(); proj.ReadFromFile(pbxPath); var guid = proj.GetUnityMainTargetGuid(); var unityframework = proj.GetUnityFrameworkTargetGuid(); proj.SetBuildProperty(guid, "ENABLE_BITCODE", "NO"); proj.SetBuildProperty(unityframework, "ENABLE_BITCODE", "NO"); proj.WriteToFile(pbxPath); } #endif } /// 빌드 메뉴 모달 — bundleVersion(↑↓ 스피너 3단) + versionCode(←→) + FGB_LIVE 체크 + Development Build 체크. 잡화점 BuildVersionDialog 이식(CDN 버튼 제거). class BuildVersionDialog : EditorWindow { int[] _verParts = new int[3]; int _codeNum; bool _ok, _live, _dev; string _msg; static int[] ParseVer(string v) { var parts = (v ?? "").Split('.'); var r = new int[3]; for (int i = 0; i < 3; i++) r[i] = (i < parts.Length && int.TryParse(parts[i].Trim(), out var n) && n >= 0) ? n : 0; return r; } public static bool Show(string message, ref string version, ref int versionCode, bool live, out bool liveOut, out bool devOut) { var w = ScriptableObject.CreateInstance(); w.titleContent = new GUIContent("WL 빌드 버전 / 심볼"); w._msg = message; w._verParts = ParseVer(version); w._codeNum = versionCode; w._live = live; w._dev = false; const float W = 470, H = 360; w.minSize = w.maxSize = new Vector2(W, H); var main = EditorGUIUtility.GetMainWindowPosition(); w.position = new Rect(main.x + (main.width - W) / 2f, main.y + (main.height - H) / 2f, W, H); w.ShowModal(); bool ok = w._ok; liveOut = w._live; devOut = w._dev; if (ok) { version = $"{w._verParts[0]}.{w._verParts[1]}.{w._verParts[2]}"; versionCode = w._codeNum; } UnityEngine.Object.DestroyImmediate(w); return ok; } void OnGUI() { EditorGUILayout.Space(8); EditorGUILayout.LabelField(_msg, EditorStyles.wordWrappedLabel); EditorGUILayout.Space(6); var bigNum = new GUIStyle(EditorStyles.boldLabel) { alignment = TextAnchor.MiddleCenter, fontSize = 18 }; EditorGUILayout.LabelField($"bundleVersion = {_verParts[0]}.{_verParts[1]}.{_verParts[2]}", EditorStyles.boldLabel); using (new EditorGUILayout.HorizontalScope()) { GUILayout.Space(12); for (int i = 0; i < 3; i++) { using (new EditorGUILayout.VerticalScope(GUILayout.Width(56))) { if (GUILayout.Button("↑", GUILayout.Width(56), GUILayout.Height(22))) _verParts[i]++; GUILayout.Label(_verParts[i].ToString(), bigNum, GUILayout.Width(56), GUILayout.Height(26)); if (GUILayout.Button("↓", GUILayout.Width(56), GUILayout.Height(22))) _verParts[i] = Mathf.Max(0, _verParts[i] - 1); } if (i < 2) GUILayout.Label(".", bigNum, GUILayout.Width(12), GUILayout.Height(70)); } GUILayout.FlexibleSpace(); } EditorGUILayout.Space(10); EditorGUILayout.LabelField("versionCode", EditorStyles.boldLabel); using (new EditorGUILayout.HorizontalScope()) { GUILayout.Space(12); if (GUILayout.Button("←", GUILayout.Width(44), GUILayout.Height(26))) _codeNum = Mathf.Max(0, _codeNum - 1); GUILayout.Label(_codeNum.ToString(), bigNum, GUILayout.Width(90), GUILayout.Height(26)); if (GUILayout.Button("→", GUILayout.Width(44), GUILayout.Height(26))) _codeNum++; GUILayout.FlexibleSpace(); } EditorGUILayout.Space(10); EditorGUILayout.LabelField("옵션", EditorStyles.boldLabel); _live = EditorGUILayout.ToggleLeft("FGB_LIVE", _live); EditorGUILayout.LabelField(" 라이브 심볼. ON 이면 ErrorLogHook 팝업 등 개발용 로직 제외 — 출시 빌드는 ON.", EditorStyles.miniLabel); _dev = EditorGUILayout.ToggleLeft("Development Build", _dev); EditorGUILayout.LabelField(" 프로파일러·스크립트 디버깅 연결 허용 — 테스트 APK 에서만.", EditorStyles.miniLabel); GUILayout.FlexibleSpace(); using (new EditorGUILayout.HorizontalScope()) { GUILayout.FlexibleSpace(); if (GUILayout.Button("취소", GUILayout.Width(80))) { _ok = false; Close(); } if (GUILayout.Button("빌드", GUILayout.Width(80))) { _ok = true; Close(); } } EditorGUILayout.Space(8); var e = Event.current; if (e.type == EventType.KeyDown) { if (e.keyCode == KeyCode.Return || e.keyCode == KeyCode.KeypadEnter) { _ok = true; Close(); } else if (e.keyCode == KeyCode.Escape) { _ok = false; Close(); } } } }