using System; using System.Diagnostics; using System.IO; using System.Reflection; using Runtime; using UnityEditor; using UnityEngine; using Debug = UnityEngine.Debug; namespace CCFramework.CrashReport.Editor { [InitializeOnLoad] public static class CrashReportBuildExtensionBootstrap { static CrashReportBuildExtensionBootstrap() { BuildWindowExtensionRegistry.Register(new CrashReportBuildExtension()); } } public sealed class CrashReportBuildExtension : IBuildWindowExtension { public string Id => "ccframework.crashreport.bugly"; public string DisplayName => "Crash/Bugly"; public int Order => 50; public bool IsEnabled(BuildWindowExtensionContext context) { BuildProfile profile = context?.profile; return profile != null && (IsAndroidProfile(profile) || IsIOSProfile(profile)); } public void DrawSection(BuildWindowExtensionContext context) { BuildProfile profile = context?.profile; if (profile == null) { EditorGUILayout.HelpBox("当前没有构建配置。", MessageType.Info); return; } CrashReportBuildSettings settings = CrashReportBuildSettingsStore.Load(); CrashReportBuglyProfileSettings profileSettings = CrashReportBuildSettingsStore.GetProfileSettings(settings, profile.profileName); bool migrated = TryMigrateLegacyProfileSettings(profile, profileSettings); CrashReportBuildSettingsStore.Normalize(profileSettings); if (migrated) { CrashReportBuildSettingsStore.Save(settings); } EditorGUI.BeginChangeCheck(); DrawRuntimeCrashConfig(); EditorGUILayout.Space(8); if (IsAndroidProfile(profile)) { DrawAndroidSection(profile, profileSettings); } else if (IsIOSProfile(profile)) { DrawIOSSection(profileSettings); } if (EditorGUI.EndChangeCheck()) { CrashReportBuildSettingsStore.Normalize(profileSettings); CrashReportBuildSettingsStore.Save(settings); AssetDatabase.SaveAssets(); } } public BuildWindowExtensionReport Preflight(BuildWindowExtensionContext context) { BuildProfile profile = context?.profile; if (profile == null) { return BuildWindowExtensionReport.Pass(); } CrashReportBuildSettings settings = CrashReportBuildSettingsStore.Load(); settings.lastBuildProfileName = profile.profileName; CrashReportBuglyProfileSettings profileSettings = CrashReportBuildSettingsStore.GetProfileSettings(settings, profile.profileName); TryMigrateLegacyProfileSettings(profile, profileSettings); CrashReportBuildSettingsStore.Normalize(profileSettings); CrashReportBuildSettingsStore.Save(settings); if (IsAndroidProfile(profile)) { BuglyAndroidSymbolUtility.ApplyAndroidSymbolSettings(profile, profileSettings, WriteLog); return profileSettings.enableAndroidSymbolArchive ? BuildWindowExtensionReport.Pass().AddMessage("Bugly Android 符号表生成已启用。") : BuildWindowExtensionReport.Pass(); } if (IsIOSProfile(profile) && profileSettings.enableIOSPod) { return BuildWindowExtensionReport.Pass().AddMessage("Bugly iOS Pod 注入已启用。"); } return BuildWindowExtensionReport.Pass(); } public BuildWindowExtensionReport PostBuild(BuildWindowExtensionContext context) { BuildProfile profile = context?.profile; if (profile == null || !context.lastBuildSuccess) { return BuildWindowExtensionReport.Pass(); } CrashReportBuildSettings settings = CrashReportBuildSettingsStore.Load(); CrashReportBuglyProfileSettings profileSettings = CrashReportBuildSettingsStore.GetProfileSettings(settings, profile.profileName); CrashReportBuildSettingsStore.Normalize(profileSettings); if (IsAndroidProfile(profile)) { return PostBuildAndroid(profile, profileSettings, context.lastBuildOutputPath); } if (IsIOSProfile(profile)) { return PostBuildIOS(profileSettings, context.lastBuildOutputPath); } return BuildWindowExtensionReport.Pass(); } private static void DrawRuntimeCrashConfig() { EditorGUILayout.LabelField("运行时上报配置", EditorStyles.boldLabel); SerializedObject serializedObject = GetCrashConfigSerializedObject(); if (serializedObject == null) { EditorGUILayout.HelpBox("未找到 CrashConfig.asset。", MessageType.Warning); return; } serializedObject.Update(); EditorGUILayout.PropertyField(serializedObject.FindProperty("EnableCrashReport"), new GUIContent("启用上报")); EditorGUILayout.PropertyField(serializedObject.FindProperty("HasDebugMode"), new GUIContent("Debug日志")); EditorGUILayout.PropertyField(serializedObject.FindProperty("BuglyAppID"), new GUIContent("Bugly App ID")); EditorGUILayout.PropertyField(serializedObject.FindProperty("BuglyChannel"), new GUIContent("Bugly Channel")); serializedObject.ApplyModifiedProperties(); } private static void DrawAndroidSection(BuildProfile profile, CrashReportBuglyProfileSettings settings) { EditorGUILayout.LabelField("Bugly Android 符号表", EditorStyles.boldLabel); settings.enableAndroidSymbolArchive = EditorGUILayout.Toggle("构建后整理符号表", settings.enableAndroidSymbolArchive); settings.androidAutoUploadSymbols = EditorGUILayout.Toggle("构建后自动上传", settings.androidAutoUploadSymbols); settings.buglyAppId = EditorGUILayout.TextField("Bugly App ID", settings.buglyAppId ?? string.Empty); settings.buglyAppKey = EditorGUILayout.PasswordField("Bugly App Key", settings.buglyAppKey ?? string.Empty); settings.buglySymbolToolPath = EditorGUILayout.TextField("符号表工具 Jar", settings.buglySymbolToolPath ?? string.Empty); settings.buglyJavaPath = EditorGUILayout.TextField("Java 命令", string.IsNullOrWhiteSpace(settings.buglyJavaPath) ? "java" : settings.buglyJavaPath); string resolvedToolPath = BuglyAndroidSymbolUtility.ResolveSymbolToolPath(settings); EditorGUILayout.LabelField("实际工具路径", string.IsNullOrEmpty(resolvedToolPath) ? "未找到" : resolvedToolPath); using (new EditorGUILayout.HorizontalScope()) { if (GUILayout.Button("下载 Zip", GUILayout.Width(100))) { Application.OpenURL(BuglyAndroidSymbolUtility.BuglyDownloadUrl); } if (GUILayout.Button("打开说明", GUILayout.Width(100))) { Application.OpenURL(BuglyAndroidSymbolUtility.BuglyToolGuideUrl); } if (GUILayout.Button("选择 Jar", GUILayout.Width(100))) { SelectBuglySymbolTool(settings); GUI.changed = true; } } using (new EditorGUILayout.HorizontalScope()) { GUILayout.FlexibleSpace(); if (GUILayout.Button("复制命令", GUILayout.Width(100))) { EditorGUIUtility.systemCopyBuffer = BuglyAndroidSymbolUtility.BuildUploadCommandTemplate(profile, settings, false); Debug.Log("Bugly 符号表上传命令已复制到剪贴板。"); } } EditorGUILayout.LabelField("上传命令模板"); EditorGUILayout.TextArea(BuglyAndroidSymbolUtility.BuildUploadCommandTemplate(profile, settings, true), GUILayout.Height(76)); } private static void DrawIOSSection(CrashReportBuglyProfileSettings settings) { EditorGUILayout.LabelField("Bugly iOS CocoaPods", EditorStyles.boldLabel); settings.enableIOSPod = EditorGUILayout.Toggle("注入 Bugly Pod", settings.enableIOSPod); settings.iosPodVersion = EditorGUILayout.TextField("Pod 版本", string.IsNullOrWhiteSpace(settings.iosPodVersion) ? "~> 2.6" : settings.iosPodVersion); settings.iosRunPodInstall = EditorGUILayout.Toggle("构建后执行 pod install", settings.iosRunPodInstall); settings.podExecutablePath = EditorGUILayout.TextField("Pod 命令", string.IsNullOrWhiteSpace(settings.podExecutablePath) ? "pod" : settings.podExecutablePath); EditorGUILayout.HelpBox("iOS 原生桥由 CrashReport 包提供;证书签名由基础构建页的 iOS 设置写入 Xcode 工程。", MessageType.Info); } private static BuildWindowExtensionReport PostBuildAndroid( BuildProfile profile, CrashReportBuglyProfileSettings settings, string buildOutputPath) { if (settings == null || !settings.enableAndroidSymbolArchive) { return BuildWindowExtensionReport.Pass(); } if (string.IsNullOrWhiteSpace(buildOutputPath)) { buildOutputPath = BuildPipelineCore.GetExpectedBuildOutputPath(profile); } BuglySymbolArchiveResult archive = BuglyAndroidSymbolUtility.ArchiveAfterBuild(profile, settings, buildOutputPath, WriteLog); if (settings.androidAutoUploadSymbols) { BuglySymbolUploadResult upload = BuglyAndroidSymbolUtility.UploadPreparedSymbols(profile, settings, archive, WriteLog); return upload != null && upload.success ? BuildWindowExtensionReport.Pass().AddMessage("Bugly 符号表已归档并上传。") : BuildWindowExtensionReport.Pass().AddWarning("Bugly 符号表已归档,但上传未成功,请查看日志。"); } return BuildWindowExtensionReport.Pass().AddMessage("Bugly 符号表已归档。"); } private static BuildWindowExtensionReport PostBuildIOS(CrashReportBuglyProfileSettings settings, string xcodeProjectPath) { if (settings == null || !settings.enableIOSPod || !settings.iosRunPodInstall) { return BuildWindowExtensionReport.Pass(); } if (string.IsNullOrWhiteSpace(xcodeProjectPath) || !Directory.Exists(xcodeProjectPath)) { return BuildWindowExtensionReport.Pass().AddWarning("未找到 iOS Xcode 工程目录,已跳过 pod install。"); } return RunPodInstall(settings, xcodeProjectPath) ? BuildWindowExtensionReport.Pass().AddMessage("pod install 执行完成。") : BuildWindowExtensionReport.Pass().AddWarning("pod install 未成功,请查看 Unity Console 日志。"); } private static bool RunPodInstall(CrashReportBuglyProfileSettings settings, string xcodeProjectPath) { string podCommand = string.IsNullOrWhiteSpace(settings.podExecutablePath) ? "pod" : settings.podExecutablePath; ProcessStartInfo startInfo = new ProcessStartInfo { FileName = podCommand, Arguments = "install", WorkingDirectory = xcodeProjectPath, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true }; try { using (Process process = Process.Start(startInfo)) { string output = process.StandardOutput.ReadToEnd(); string error = process.StandardError.ReadToEnd(); process.WaitForExit(); if (!string.IsNullOrWhiteSpace(output)) { Debug.Log(output); } if (!string.IsNullOrWhiteSpace(error)) { Debug.LogWarning(error); } return process.ExitCode == 0; } } catch (Exception e) { Debug.LogWarning($"[CrashReport] 执行 pod install 失败:{e.Message}"); return false; } } private static SerializedObject GetCrashConfigSerializedObject() { const string path = "Assets/Resources"; const string assetPath = "Assets/Resources/CrashConfig.asset"; CrashConfig config = AssetDatabase.LoadAssetAtPath(assetPath); if (config == null) { if (!Directory.Exists(path)) { Directory.CreateDirectory(path); } config = ScriptableObject.CreateInstance(); AssetDatabase.CreateAsset(config, assetPath); AssetDatabase.Refresh(); } return config == null ? null : new SerializedObject(config); } private static bool TryMigrateLegacyProfileSettings( BuildProfile profile, CrashReportBuglyProfileSettings settings) { if (profile == null || settings == null) { return false; } bool changed = false; if (!settings.enableAndroidSymbolArchive && profile.enableBuglySymbolArchive) { settings.enableAndroidSymbolArchive = true; changed = true; } if (!settings.androidAutoUploadSymbols && profile.buglyAutoUploadSymbols) { settings.androidAutoUploadSymbols = true; changed = true; } if (string.IsNullOrWhiteSpace(settings.buglyAppId) && !string.IsNullOrWhiteSpace(profile.buglyAppId)) { settings.buglyAppId = profile.buglyAppId; changed = true; } if (string.IsNullOrWhiteSpace(settings.buglyAppKey) && !string.IsNullOrWhiteSpace(profile.buglyAppKey)) { settings.buglyAppKey = profile.buglyAppKey; changed = true; } if (IsDefaultSymbolToolPath(settings.buglySymbolToolPath) && !string.IsNullOrWhiteSpace(profile.buglySymbolToolPath)) { settings.buglySymbolToolPath = profile.buglySymbolToolPath; changed = true; } if (string.Equals(settings.buglyJavaPath, "java", StringComparison.Ordinal) && !string.IsNullOrWhiteSpace(profile.buglyJavaPath) && !string.Equals(profile.buglyJavaPath, "java", StringComparison.Ordinal)) { settings.buglyJavaPath = profile.buglyJavaPath; changed = true; } return changed; } private static bool IsDefaultSymbolToolPath(string value) { return string.IsNullOrWhiteSpace(value) || string.Equals(value, "Tools/BuglySymbolTool/buglyqq-upload-symbol.jar", StringComparison.OrdinalIgnoreCase); } private static bool IsAndroidProfile(BuildProfile profile) { return string.Equals(ResolveProfilePlatformName(profile), "Android", StringComparison.OrdinalIgnoreCase); } private static bool IsIOSProfile(BuildProfile profile) { string platformName = ResolveProfilePlatformName(profile); return string.Equals(platformName, "iOS", StringComparison.OrdinalIgnoreCase) || string.Equals(platformName, "iPhone", StringComparison.OrdinalIgnoreCase); } private static string ResolveProfilePlatformName(BuildProfile profile) { if (profile == null) { return string.Empty; } Type profileType = profile.GetType(); string targetPlatform = GetStringField(profileType, profile, "targetPlatform"); string buildTargetGroup = GetStringField(profileType, profile, "buildTargetGroup"); string buildTarget = GetStringField(profileType, profile, "buildTarget"); bool hasExplicitPlatformValue = !string.IsNullOrWhiteSpace(targetPlatform) || !string.IsNullOrWhiteSpace(buildTargetGroup) || !string.IsNullOrWhiteSpace(buildTarget); if (IsPlatformValue(targetPlatform, "iOS", "iPhone") || IsPlatformValue(buildTargetGroup, "iOS") || IsPlatformValue(buildTarget, "iOS", "iPhone")) { return "iOS"; } if (IsPlatformValue(targetPlatform, "Android") || IsPlatformValue(buildTargetGroup, "Android") || IsPlatformValue(buildTarget, "Android")) { return "Android"; } if (hasExplicitPlatformValue) { return string.Empty; } if (EditorUserBuildSettings.activeBuildTarget == BuildTarget.iOS) { return "iOS"; } return EditorUserBuildSettings.activeBuildTarget == BuildTarget.Android ? "Android" : string.Empty; } private static string GetStringField(Type ownerType, object instance, string fieldName) { FieldInfo field = ownerType.GetField(fieldName, BindingFlags.Public | BindingFlags.Instance); return field?.GetValue(instance) as string ?? string.Empty; } private static bool IsPlatformValue(string value, params string[] expectedValues) { if (string.IsNullOrWhiteSpace(value)) { return false; } foreach (string expectedValue in expectedValues) { if (string.Equals(value, expectedValue, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } private static void SelectBuglySymbolTool(CrashReportBuglyProfileSettings settings) { string selectedPath = EditorUtility.OpenFilePanel("选择 Bugly 符号表工具", Application.dataPath, "jar"); if (string.IsNullOrEmpty(selectedPath)) { return; } settings.buglySymbolToolPath = BuglyAndroidSymbolUtility.ToProfilePath(selectedPath); } private static void WriteLog(string message, LogType type = LogType.Log) { switch (type) { case LogType.Error: case LogType.Exception: Debug.LogError(message); break; case LogType.Warning: Debug.LogWarning(message); break; default: Debug.Log(message); break; } } } }