Project_WL/Assets/Editor/AutoBuild.cs

464 lines
23 KiB
C#

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;
/// <summary>
/// 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)
/// </summary>
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);
}
/// <summary> 마지막 빌드 버전/코드 — 백업 txt 가 권위. 백업이 없을 때(최초 1회)만 PlayerSettings. </summary>
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#";
}
/// <summary> FGB_LIVE 만 넣고/빼고 나머지 심볼은 보존. 원문은 s_SavedDefines 에 보관 → Common_End 에서 복원. </summary>
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);
}
/// <summary> 빌드 버전 지정. 원래 값은 s_SavedBundleVersion 에 보관 → Common_End 에서 복원 (에디터는 9.9.9 = dev 서버 유지 · 1.0.0 등이 남으면 Play 시 업데이트 팝업). </summary>
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();
}
/// <summary> 네이티브 디버그 심볼(symbols.zip) — 라이브 AAB 만 ON (Play Console·Cloud Diagnostics 역심볼화). IL2CPP 스택은 파일·라인까지 상시. </summary>
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 ─────────────────────────
/// <summary> WL_Local 프로필(Remote.* 를 Local.* 경로로) 활성화 → 전 그룹이 StreamingAssets 로 들어간다. 없으면 Default 복제로 생성. </summary>
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;
}
/// <summary> 앱 빌드 직전 Addressables 콘텐츠 빌드(WL_Local). SBP 캐시 비우고 풀빌드 → stale 번들 차단. 실패 시 false. </summary>
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}";
/// <summary> 빌드 파이프라인 1회 실행. 다이얼로그 뒤 호출 — s_Live/s_DevBuild/s_ManualVersionCode 가 이미 정해진 상태. </summary>
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
}
/// <summary> 빌드 메뉴 모달 — bundleVersion(↑↓ 스피너 3단) + versionCode(←→) + FGB_LIVE 체크 + Development Build 체크. 잡화점 BuildVersionDialog 이식(CDN 버튼 제거). </summary>
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<BuildVersionDialog>();
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(); }
}
}
}