1 Commits
1.0.3 ... upm

Author SHA1 Message Date
08ea53580d feat: move bugly build settings into crashreport 2026-06-14 18:18:05 +08:00
9 changed files with 1325 additions and 8 deletions

View File

@@ -25,3 +25,16 @@
### 修复
* 修复 `CHANGELOG.md.meta``package.json.meta` 和 Commercialization 包重复导致的 UPM GUID 冲突告警。
# [1.0.4]
### 新增
* 新增 Crash/Bugly 构建面板扩展,由 CrashReport 包自身维护 Bugly 构建配置。
* 支持按构建 profile 保存 Android 符号表归档/上传配置。
* 支持按构建 profile 配置 iOS Bugly Pod 注入版本和构建后 `pod install`
### 调整
* iOS Bugly Pod 后处理改为读取 CrashReport 构建配置,不再固定写入单一版本。
* 声明对 `com.foldcc.cc-framework.common` 的构建面板 API 依赖。

View File

@@ -0,0 +1,651 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using UnityEditor;
using UnityEngine;
using Debug = UnityEngine.Debug;
namespace CCFramework.CrashReport.Editor
{
public static class BuglyAndroidSymbolUtility
{
public delegate void LogHandler(string message, LogType type = LogType.Log);
public const string BuglyDownloadUrl = "https://bugly.tds.tencent.com/docs/assets/files/bugly_symbol_oversea_v3.4.23-a449c7a62d7792de76fca1af623584f5.zip";
public const string BuglyToolGuideUrl = "https://bugly.tds.tencent.com/docs/tutorial/symbol/tool/";
private const string SymbolFolderSuffix = "_bugly_symbols";
private const string SymbolPackageSuffix = "_bugly_symbols.zip";
private const string UploadStagingDirectory = "Library/BuglySymbolUpload";
public static void ApplyAndroidSymbolSettings(BuildProfile profile, CrashReportBuglyProfileSettings settings, LogHandler log)
{
if (!IsAndroidProfile(profile) || settings == null || !settings.enableAndroidSymbolArchive)
{
return;
}
EditorUserBuildSettings.androidCreateSymbols = AndroidCreateSymbols.Debugging;
WriteLog(log, "已启用 Android Debugging 符号表生成,构建后会产出 native-debug-symbols.zip。");
}
public static BuglySymbolArchiveResult ArchiveAfterBuild(
BuildProfile profile,
CrashReportBuglyProfileSettings settings,
string buildOutputPath,
LogHandler log)
{
BuglySymbolArchiveResult result = new BuglySymbolArchiveResult();
if (!IsAndroidProfile(profile) || settings == null || !settings.enableAndroidSymbolArchive)
{
result.message = "当前不是 Android 构建或未启用 Bugly 符号表归档。";
WriteLog(log, result.message);
return result;
}
string resolvedBuildOutputPath = ResolveFullPath(buildOutputPath);
string buildDirectory = Path.GetDirectoryName(resolvedBuildOutputPath);
string buildName = Path.GetFileNameWithoutExtension(resolvedBuildOutputPath);
string archiveDirectory = Path.Combine(buildDirectory, buildName + SymbolFolderSuffix);
Directory.CreateDirectory(archiveDirectory);
string nativeSymbolPath = FindNativeDebugSymbols(profile);
string mappingPath = FindMappingFile(profile, resolvedBuildOutputPath);
if (!string.IsNullOrWhiteSpace(nativeSymbolPath))
{
result.nativeSymbolPath = Path.Combine(archiveDirectory, buildName + "_native-debug-symbols.zip");
File.Copy(nativeSymbolPath, result.nativeSymbolPath, true);
WriteLog(log, $"已归档 Bugly SO 符号表:{result.nativeSymbolPath}");
}
else
{
WriteLog(log, "未找到 native-debug-symbols.zipNative Crash 可能无法还原。", LogType.Warning);
}
if (!string.IsNullOrWhiteSpace(mappingPath))
{
result.mappingPath = Path.Combine(archiveDirectory, buildName + "_mapping.txt");
File.Copy(mappingPath, result.mappingPath, true);
WriteLog(log, $"已归档 Bugly mapping 文件:{result.mappingPath}");
}
result.archiveDirectory = archiveDirectory;
result.manifestPath = WriteManifest(profile, resolvedBuildOutputPath, archiveDirectory, result);
result.packagePath = CreatePackage(archiveDirectory, Path.Combine(buildDirectory, buildName + SymbolPackageSuffix));
result.hasSymbols = File.Exists(result.nativeSymbolPath) || File.Exists(result.mappingPath);
result.message = result.hasSymbols ? "Bugly 符号表归档完成。" : "Bugly 符号表归档完成,但没有找到可上传的符号表文件。";
WriteLog(log, $"Bugly 符号表包:{result.packagePath}");
return result;
}
public static BuglySymbolUploadResult UploadPreparedSymbols(
BuildProfile profile,
CrashReportBuglyProfileSettings settings,
BuglySymbolArchiveResult archive,
LogHandler log)
{
if (profile == null || settings == null)
{
return FailUpload("构建配置为空,无法上传 Bugly 符号表。", log);
}
if (archive == null || !archive.hasSymbols)
{
return FailUpload("没有可上传的 Bugly 符号表文件。", log);
}
string javaPath = string.IsNullOrWhiteSpace(settings.buglyJavaPath) ? "java" : settings.buglyJavaPath.Trim();
string toolPath = ResolveSymbolToolPath(settings);
if (string.IsNullOrWhiteSpace(settings.buglyAppId))
{
return FailUpload("Bugly App ID 为空。", log);
}
if (string.IsNullOrWhiteSpace(settings.buglyAppKey))
{
return FailUpload("Bugly App Key 为空。", log);
}
if (string.IsNullOrWhiteSpace(toolPath) || !File.Exists(toolPath))
{
return FailUpload($"Bugly 符号表工具不存在:{toolPath}", log);
}
BuglySymbolArchiveResult uploadArchive = PrepareUploadInputs(profile, archive, log);
List<string> arguments = new List<string>
{
"-jar",
Quote(toolPath),
"-appid",
Quote(settings.buglyAppId.Trim()),
"-appkey",
Quote(settings.buglyAppKey.Trim()),
"-version",
Quote(profile.version),
"-buildNo",
Quote(profile.buildNumber.ToString()),
"-platform",
"Android"
};
if (PathExists(uploadArchive.nativeSymbolPath))
{
arguments.Add("-inputSymbol");
arguments.Add(Quote(uploadArchive.nativeSymbolPath));
}
if (File.Exists(uploadArchive.mappingPath))
{
arguments.Add("-inputMapping");
arguments.Add(Quote(uploadArchive.mappingPath));
}
string argumentText = string.Join(" ", arguments);
WriteLog(log, $"开始上传 Bugly 符号表:{javaPath} {MaskUploadArguments(argumentText)}");
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = javaPath,
Arguments = argumentText,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
StandardOutputEncoding = GetBuglyToolOutputEncoding(),
StandardErrorEncoding = GetBuglyToolOutputEncoding()
};
try
{
using (Process process = Process.Start(startInfo))
{
string output = process.StandardOutput.ReadToEnd();
string error = process.StandardError.ReadToEnd();
process.WaitForExit();
BuglySymbolUploadResult result = new BuglySymbolUploadResult
{
success = process.ExitCode == 0 && IsBuglyUploadOutputSuccessful(output, error),
exitCode = process.ExitCode,
output = output,
error = error,
command = javaPath + " " + MaskUploadArguments(argumentText)
};
result.logPath = WriteUploadLog(archive.archiveDirectory, result);
if (result.success)
{
WriteLog(log, $"Bugly 符号表上传完成,日志:{result.logPath}");
}
else
{
WriteLog(log, $"Bugly 符号表上传失败ExitCode={result.exitCode},日志:{result.logPath}", LogType.Error);
}
return result;
}
}
catch (Exception e)
{
return FailUpload($"执行 Bugly 符号表上传失败:{e.Message}", log);
}
}
public static string BuildUploadCommandTemplate(BuildProfile profile, CrashReportBuglyProfileSettings settings, bool maskSecret)
{
if (profile == null || settings == null)
{
return string.Empty;
}
string buildOutputPath = ResolveFullPath(BuildPipelineCore.GetExpectedBuildOutputPath(profile));
string buildDirectory = Path.GetDirectoryName(buildOutputPath);
string buildName = Path.GetFileNameWithoutExtension(buildOutputPath);
string archiveDirectory = Path.Combine(buildDirectory, buildName + SymbolFolderSuffix);
string appKey = string.IsNullOrWhiteSpace(settings.buglyAppKey) ? "<Bugly App Key>" : settings.buglyAppKey.Trim();
if (maskSecret && !string.IsNullOrWhiteSpace(settings.buglyAppKey))
{
appKey = "******";
}
return (string.IsNullOrWhiteSpace(settings.buglyJavaPath) ? "java" : settings.buglyJavaPath.Trim()) +
" -jar " + Quote(ResolveSymbolToolPath(settings)) +
" -appid " + Quote(string.IsNullOrWhiteSpace(settings.buglyAppId) ? "<Bugly App ID>" : settings.buglyAppId.Trim()) +
" -appkey " + Quote(appKey) +
" -version " + Quote(profile.version) +
" -buildNo " + Quote(profile.buildNumber.ToString()) +
" -platform Android" +
" -inputSymbol " + Quote(Path.Combine(archiveDirectory, buildName + "_native-debug-symbols.zip")) +
" -inputMapping " + Quote(Path.Combine(archiveDirectory, buildName + "_mapping.txt"));
}
public static string ResolveSymbolToolPath(CrashReportBuglyProfileSettings settings)
{
if (settings != null && !string.IsNullOrWhiteSpace(settings.buglySymbolToolPath))
{
string configuredPath = ResolveFullPath(settings.buglySymbolToolPath);
if (File.Exists(configuredPath))
{
return configuredPath;
}
}
string bundledRoot = Path.Combine(GetProjectRoot(), "Tools", "BuglySymbolTool");
if (!Directory.Exists(bundledRoot))
{
return string.Empty;
}
string newestPath = string.Empty;
DateTime newestWriteTime = DateTime.MinValue;
foreach (string jarPath in Directory.GetFiles(bundledRoot, "*.jar", SearchOption.AllDirectories))
{
DateTime writeTime = File.GetLastWriteTimeUtc(jarPath);
if (writeTime > newestWriteTime)
{
newestPath = jarPath;
newestWriteTime = writeTime;
}
}
return newestPath;
}
public static string ToProfilePath(string path)
{
if (string.IsNullOrWhiteSpace(path))
{
return string.Empty;
}
string fullPath = ResolveFullPath(path);
string projectRoot = GetProjectRoot().TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
string rootPrefix = projectRoot + Path.DirectorySeparatorChar;
if (fullPath.StartsWith(rootPrefix, StringComparison.OrdinalIgnoreCase))
{
return fullPath.Substring(rootPrefix.Length).Replace(Path.DirectorySeparatorChar, '/');
}
return path;
}
private static bool IsAndroidProfile(BuildProfile profile)
{
if (profile == null)
{
return false;
}
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);
return IsPlatformValue(targetPlatform, "Android") ||
IsPlatformValue(buildTargetGroup, "Android") ||
IsPlatformValue(buildTarget, "Android") ||
(!hasExplicitPlatformValue && EditorUserBuildSettings.activeBuildTarget == BuildTarget.Android);
}
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, string expectedValue)
{
return !string.IsNullOrWhiteSpace(value) &&
string.Equals(value, expectedValue, StringComparison.OrdinalIgnoreCase);
}
private static string FindNativeDebugSymbols(BuildProfile profile)
{
string buildType = profile != null && profile.isDevelopment ? "debug" : "release";
string preferredPath = Path.Combine(
GetProjectRoot(),
"Library",
"Bee",
"Android",
"Prj",
"IL2CPP",
"Gradle",
"launcher",
"build",
"outputs",
"native-debug-symbols",
buildType,
"native-debug-symbols.zip");
return File.Exists(preferredPath)
? preferredPath
: FindNewestFile(Path.Combine(GetProjectRoot(), "Library", "Bee", "Android"), "native-debug-symbols.zip");
}
private static string FindMappingFile(BuildProfile profile, string buildOutputPath)
{
string buildDirectory = Path.GetDirectoryName(buildOutputPath);
string newestOutputMapping = FindNewestFile(buildDirectory, "*mapping*.txt");
if (!string.IsNullOrEmpty(newestOutputMapping))
{
return newestOutputMapping;
}
string buildType = profile != null && profile.isDevelopment ? "debug" : "release";
string preferredRoot = Path.Combine(
GetProjectRoot(),
"Library",
"Bee",
"Android",
"Prj",
"IL2CPP",
"Gradle",
"launcher",
"build",
"outputs",
"mapping",
buildType);
string preferredMapping = FindNewestFile(preferredRoot, "mapping.txt");
return !string.IsNullOrEmpty(preferredMapping)
? preferredMapping
: FindNewestFile(Path.Combine(GetProjectRoot(), "Library", "Bee", "Android"), "mapping.txt");
}
private static string FindNewestFile(string root, string fileName)
{
if (string.IsNullOrEmpty(root) || !Directory.Exists(root))
{
return string.Empty;
}
string newestPath = string.Empty;
DateTime newestWriteTime = DateTime.MinValue;
foreach (string file in Directory.GetFiles(root, fileName, SearchOption.AllDirectories))
{
DateTime writeTime = File.GetLastWriteTimeUtc(file);
if (writeTime > newestWriteTime)
{
newestPath = file;
newestWriteTime = writeTime;
}
}
return newestPath;
}
private static string WriteManifest(BuildProfile profile, string buildOutputPath, string archiveDirectory, BuglySymbolArchiveResult result)
{
BuglySymbolManifest manifest = new BuglySymbolManifest
{
productName = profile.productName,
bundleIdentifier = profile.bundleIdentifier,
version = profile.version,
buildNumber = profile.buildNumber,
buildOutputPath = buildOutputPath,
buildOutputSha256 = File.Exists(buildOutputPath) ? GetSHA256Hash(buildOutputPath) : string.Empty,
nativeSymbolPath = result.nativeSymbolPath,
nativeSymbolSha256 = File.Exists(result.nativeSymbolPath) ? GetSHA256Hash(result.nativeSymbolPath) : string.Empty,
mappingPath = result.mappingPath,
mappingSha256 = File.Exists(result.mappingPath) ? GetSHA256Hash(result.mappingPath) : string.Empty,
createdAt = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")
};
string manifestPath = Path.Combine(archiveDirectory, "bugly-symbol-manifest.json");
File.WriteAllText(manifestPath, JsonUtility.ToJson(manifest, true), Encoding.UTF8);
return manifestPath;
}
private static string CreatePackage(string archiveDirectory, string packagePath)
{
if (File.Exists(packagePath))
{
File.Delete(packagePath);
}
ZipFile.CreateFromDirectory(
archiveDirectory,
packagePath,
System.IO.Compression.CompressionLevel.Optimal,
false);
return packagePath;
}
private static BuglySymbolArchiveResult PrepareUploadInputs(BuildProfile profile, BuglySymbolArchiveResult archive, LogHandler log)
{
string stagingRoot = Path.Combine(GetProjectRoot(), UploadStagingDirectory.Replace("/", Path.DirectorySeparatorChar.ToString()));
string stagingDirectory = Path.Combine(stagingRoot, BuildSafeStagingName(profile) + "_" + DateTime.Now.ToString("yyyyMMddHHmmss"));
Directory.CreateDirectory(stagingDirectory);
BuglySymbolArchiveResult uploadArchive = new BuglySymbolArchiveResult
{
hasSymbols = archive.hasSymbols,
archiveDirectory = archive.archiveDirectory,
packagePath = archive.packagePath,
manifestPath = archive.manifestPath,
message = archive.message
};
if (File.Exists(archive.nativeSymbolPath))
{
string symbolDirectory = Path.Combine(stagingDirectory, "symbols");
Directory.CreateDirectory(symbolDirectory);
ZipFile.ExtractToDirectory(archive.nativeSymbolPath, symbolDirectory);
uploadArchive.nativeSymbolPath = symbolDirectory;
WriteLog(log, $"已解压 Bugly SO 符号表到英文临时目录:{symbolDirectory}");
}
if (File.Exists(archive.mappingPath))
{
uploadArchive.mappingPath = Path.Combine(stagingDirectory, "mapping.txt");
File.Copy(archive.mappingPath, uploadArchive.mappingPath, true);
}
return uploadArchive;
}
private static string BuildSafeStagingName(BuildProfile profile)
{
string rawName = profile == null ? "bugly" : profile.profileName + "_" + profile.version + "_" + profile.buildNumber;
StringBuilder builder = new StringBuilder(rawName.Length);
foreach (char ch in rawName)
{
builder.Append(char.IsLetterOrDigit(ch) || ch == '_' || ch == '-' || ch == '.' ? ch : '_');
}
return builder.ToString();
}
private static string WriteUploadLog(string archiveDirectory, BuglySymbolUploadResult result)
{
string logDirectory = Directory.Exists(archiveDirectory) ? archiveDirectory : GetProjectRoot();
string logPath = Path.Combine(logDirectory, "bugly-symbol-upload.log");
File.WriteAllText(
logPath,
$"Time: {DateTime.Now:yyyy-MM-dd HH:mm:ss}\nCommand: {result.command}\nExitCode: {result.exitCode}\n--- Output ---\n{result.output}\n--- Error ---\n{result.error}",
Encoding.UTF8);
return logPath;
}
private static BuglySymbolUploadResult FailUpload(string message, LogHandler log)
{
WriteLog(log, message, LogType.Error);
return new BuglySymbolUploadResult
{
success = false,
exitCode = -1,
error = message
};
}
private static bool PathExists(string path)
{
return File.Exists(path) || Directory.Exists(path);
}
private static Encoding GetBuglyToolOutputEncoding()
{
try
{
return Encoding.GetEncoding("GB18030");
}
catch
{
return Encoding.Default;
}
}
private static bool IsBuglyUploadOutputSuccessful(string output, string error)
{
string combinedText = (output ?? string.Empty) + "\n" + (error ?? string.Empty);
return combinedText.IndexOf("plugin execute result status: failure", StringComparison.OrdinalIgnoreCase) < 0 &&
combinedText.IndexOf("event_res:failure", StringComparison.OrdinalIgnoreCase) < 0 &&
combinedText.IndexOf("##[error]", StringComparison.OrdinalIgnoreCase) < 0 &&
combinedText.IndexOf("errorMsg is", StringComparison.OrdinalIgnoreCase) < 0;
}
private static string ResolveFullPath(string path)
{
if (string.IsNullOrWhiteSpace(path))
{
return string.Empty;
}
string normalizedPath = path.Replace("{inproject}: ", string.Empty);
return Path.IsPathRooted(normalizedPath)
? Path.GetFullPath(normalizedPath)
: Path.GetFullPath(Path.Combine(GetProjectRoot(), normalizedPath));
}
private static string GetProjectRoot()
{
return Directory.GetParent(Application.dataPath).FullName;
}
private static string Quote(string value)
{
return string.IsNullOrEmpty(value) ? "\"\"" : "\"" + value.Replace("\"", "\\\"") + "\"";
}
private static string MaskUploadArguments(string arguments)
{
return MaskArgumentValue(arguments, "-appkey");
}
private static string MaskArgumentValue(string arguments, string key)
{
int keyIndex = arguments.IndexOf(key, StringComparison.OrdinalIgnoreCase);
if (keyIndex < 0)
{
return arguments;
}
int valueStart = keyIndex + key.Length;
while (valueStart < arguments.Length && char.IsWhiteSpace(arguments[valueStart]))
{
valueStart++;
}
if (valueStart >= arguments.Length)
{
return arguments;
}
int tokenEnd = valueStart;
if (arguments[valueStart] == '"')
{
tokenEnd = arguments.IndexOf('"', valueStart + 1);
return tokenEnd > valueStart
? arguments.Substring(0, valueStart + 1) + "******" + arguments.Substring(tokenEnd)
: arguments;
}
while (tokenEnd < arguments.Length && !char.IsWhiteSpace(arguments[tokenEnd]))
{
tokenEnd++;
}
return arguments.Substring(0, valueStart) + "******" + arguments.Substring(tokenEnd);
}
private static string GetSHA256Hash(string filePath)
{
using (SHA256 sha256 = SHA256.Create())
using (FileStream stream = File.OpenRead(filePath))
{
return BitConverter.ToString(sha256.ComputeHash(stream)).Replace("-", "").ToLowerInvariant();
}
}
private static void WriteLog(LogHandler log, string message, LogType type = LogType.Log)
{
if (log != null)
{
log(message, type);
return;
}
switch (type)
{
case LogType.Error:
case LogType.Exception:
Debug.LogError(message);
break;
case LogType.Warning:
Debug.LogWarning(message);
break;
default:
Debug.Log(message);
break;
}
}
}
[Serializable]
public class BuglySymbolArchiveResult
{
public bool hasSymbols;
public string archiveDirectory;
public string packagePath;
public string nativeSymbolPath;
public string mappingPath;
public string manifestPath;
public string message;
}
[Serializable]
public class BuglySymbolUploadResult
{
public bool success;
public int exitCode;
public string command;
public string output;
public string error;
public string logPath;
}
[Serializable]
public class BuglySymbolManifest
{
public string productName;
public string bundleIdentifier;
public string version;
public int buildNumber;
public string buildOutputPath;
public string buildOutputSha256;
public string nativeSymbolPath;
public string nativeSymbolSha256;
public string mappingPath;
public string mappingSha256;
public string createdAt;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ecfd0727debd42f7bde54f6b3d1f5f74
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -2,6 +2,7 @@
using System;
using System.IO;
using CCFramework.CrashReport.Editor;
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEngine;
@@ -10,7 +11,6 @@ namespace Editor
{
public static class CrashPostProcessBuildIOS
{
private const string PodLine = " pod 'Bugly', '~> 2.6'";
private const string UnityFrameworkTarget = "target 'UnityFramework' do";
[PostProcessBuild(980)]
@@ -21,13 +21,21 @@ namespace Editor
return;
}
EnsureBuglyPod(pathToBuiltProject);
CrashReportBuglyProfileSettings settings = CrashReportBuildSettingsStore.GetLastBuildProfileSettings();
if (settings == null || !settings.enableIOSPod)
{
return;
}
EnsureBuglyPod(pathToBuiltProject, settings.iosPodVersion);
}
private static void EnsureBuglyPod(string pathToBuiltProject)
public static void EnsureBuglyPod(string pathToBuiltProject, string podVersion)
{
string podfilePath = Path.Combine(pathToBuiltProject, "Podfile");
string content = File.Exists(podfilePath) ? File.ReadAllText(podfilePath) : string.Empty;
string normalizedVersion = string.IsNullOrWhiteSpace(podVersion) ? "~> 2.6" : podVersion.Trim();
string podLine = $" pod 'Bugly', '{normalizedVersion}'";
if (content.Contains("pod 'Bugly'") || content.Contains("pod \"Bugly\""))
{
@@ -40,12 +48,12 @@ namespace Editor
"platform :ios, '9.0'" + Environment.NewLine +
Environment.NewLine +
UnityFrameworkTarget + Environment.NewLine +
PodLine + Environment.NewLine +
podLine + Environment.NewLine +
"end" + Environment.NewLine);
}
else if (content.Contains(UnityFrameworkTarget))
{
content = content.Replace(UnityFrameworkTarget, UnityFrameworkTarget + Environment.NewLine + PodLine);
content = content.Replace(UnityFrameworkTarget, UnityFrameworkTarget + Environment.NewLine + podLine);
File.WriteAllText(podfilePath, content);
}
else
@@ -53,11 +61,11 @@ namespace Editor
File.AppendAllText(podfilePath,
Environment.NewLine +
UnityFrameworkTarget + Environment.NewLine +
PodLine + Environment.NewLine +
podLine + Environment.NewLine +
"end" + Environment.NewLine);
}
Debug.Log("CrashReport iOS 已写入 Bugly CocoaPods 依赖,请在 Xcode 构建前执行 pod install。");
Debug.Log($"CrashReport iOS 已写入 Bugly CocoaPods 依赖{normalizedVersion},请在 Xcode 构建前执行 pod install。");
}
}
}

View File

@@ -0,0 +1,481 @@
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;
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2b8fdf55210d41f6906198c6f5e3901b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,130 @@
using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
namespace CCFramework.CrashReport.Editor
{
[Serializable]
public sealed class CrashReportBuildSettings
{
public string lastBuildProfileName = string.Empty;
public List<CrashReportBuglyProfileSettings> profiles = new List<CrashReportBuglyProfileSettings>();
}
[Serializable]
public sealed class CrashReportBuglyProfileSettings
{
public string profileName = "default";
public bool enableAndroidSymbolArchive = true;
public bool androidAutoUploadSymbols = false;
public string buglyAppId = string.Empty;
public string buglyAppKey = string.Empty;
public string buglySymbolToolPath = "Tools/BuglySymbolTool/buglyqq-upload-symbol.jar";
public string buglyJavaPath = "java";
public bool enableIOSPod = true;
public string iosPodVersion = "~> 2.6";
public bool iosRunPodInstall = false;
public string podExecutablePath = "pod";
}
public static class CrashReportBuildSettingsStore
{
private const string SettingsDirectory = "ProjectSettings/CrashReport";
private const string SettingsFileName = "crashreport-build-settings.json";
public static string SettingsPath => Path.Combine(SettingsDirectory, SettingsFileName);
public static CrashReportBuildSettings Load()
{
if (!File.Exists(SettingsPath))
{
return new CrashReportBuildSettings();
}
try
{
string json = File.ReadAllText(SettingsPath);
CrashReportBuildSettings settings = JsonUtility.FromJson<CrashReportBuildSettings>(json);
return settings ?? new CrashReportBuildSettings();
}
catch (Exception e)
{
Debug.LogWarning($"[CrashReport] 构建配置读取失败,将使用默认配置:{e.Message}");
return new CrashReportBuildSettings();
}
}
public static void Save(CrashReportBuildSettings settings)
{
if (!Directory.Exists(SettingsDirectory))
{
Directory.CreateDirectory(SettingsDirectory);
}
File.WriteAllText(SettingsPath, JsonUtility.ToJson(settings ?? new CrashReportBuildSettings(), true));
}
public static CrashReportBuglyProfileSettings GetProfileSettings(CrashReportBuildSettings settings, string profileName)
{
settings = settings ?? new CrashReportBuildSettings();
string key = string.IsNullOrWhiteSpace(profileName) ? "default" : profileName;
foreach (CrashReportBuglyProfileSettings profileSettings in settings.profiles)
{
if (profileSettings != null && string.Equals(profileSettings.profileName, key, StringComparison.Ordinal))
{
Normalize(profileSettings);
return profileSettings;
}
}
CrashReportBuglyProfileSettings created = new CrashReportBuglyProfileSettings
{
profileName = key
};
Normalize(created);
settings.profiles.Add(created);
return created;
}
public static CrashReportBuglyProfileSettings GetLastBuildProfileSettings()
{
CrashReportBuildSettings settings = Load();
return GetProfileSettings(settings, settings.lastBuildProfileName);
}
public static void Normalize(CrashReportBuglyProfileSettings settings)
{
if (settings == null)
{
return;
}
if (string.IsNullOrWhiteSpace(settings.profileName))
{
settings.profileName = "default";
}
if (string.IsNullOrWhiteSpace(settings.buglyJavaPath))
{
settings.buglyJavaPath = "java";
}
if (string.IsNullOrWhiteSpace(settings.buglySymbolToolPath))
{
settings.buglySymbolToolPath = "Tools/BuglySymbolTool/buglyqq-upload-symbol.jar";
}
if (string.IsNullOrWhiteSpace(settings.iosPodVersion))
{
settings.iosPodVersion = "~> 2.6";
}
if (string.IsNullOrWhiteSpace(settings.podExecutablePath))
{
settings.podExecutablePath = "pod";
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bcb8cf0a589747d490149a0769a07a42
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -2,7 +2,7 @@
"name": "com.foldcc.cc-framework.crashreport",
"displayName": "CC-Framework.CrashReport",
"description": "Crash检测 异常上报",
"version": "1.0.3",
"version": "1.0.4",
"unity": "2022.3",
"license": "MIT",
"repository": {
@@ -16,6 +16,7 @@
},
"dependencies":
{
"com.foldcc.cc-framework.common": "1.0.5"
},
"keywords": [
"Framework"