Files
CC-Framework.CrashReport/Assets/Editor/CrashReportBuildExtension.cs

482 lines
20 KiB
C#

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<CrashConfig>(assetPath);
if (config == null)
{
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
config = ScriptableObject.CreateInstance<CrashConfig>();
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;
}
}
}
}