feat: move bugly build settings into crashreport

This commit is contained in:
2026-06-14 18:18:05 +08:00
parent c6f37c94ba
commit 08ea53580d
9 changed files with 1325 additions and 8 deletions

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;
}
}