Files
plugin-library/Assets/00.StaryEvoTools/Editor/Build/BuildApkWindow.cs
zhangzheng caf86d2e2d 11
2026-03-18 16:31:57 +08:00

641 lines
21 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Sirenix.OdinInspector;
using Sirenix.OdinInspector.Editor;
using UnityEditor;
using UnityEditor.Build.Reporting;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace Stary.Evo.Editor
{
/// <summary>
/// Android APK打包工具窗口
/// </summary>
public class BuildApkWindow : OdinEditorWindow
{
#region
private static OdinEditorWindow window;
private bool _isBuilding;
private UnityEditor.Build.Reporting.BuildReport _lastBuildReport;
private string[] _scenePaths;
[Title("设备类型选择", titleAlignment: TitleAlignments.Centered)] [EnumToggleButtons, HideLabel]
public DeviceType deviceType = DeviceType.Xreal;
[Title("包裹列表", titleAlignment: TitleAlignments.Centered)]
[LabelText("Domain子包")]
[ValueDropdown("GetAvailablePackageNames")]
[OnValueChanged("OnPackageNameChanged")]
public string selectedPackageName;
[Title("打包配置", titleAlignment: TitleAlignments.Centered)] [ToggleLeft] [LabelText("自动递增版本号")]
public bool autoIncrementVersion = true;
[Title("打包配置", titleAlignment: TitleAlignments.Centered)] [ToggleLeft] [LabelText("是否添加水印")]
public bool isWatermark = false;
[ToggleLeft] [LabelText("构建完成后打开文件夹")] public bool openFolderOnComplete = true;
[BoxGroup("缓存管理", centerLabel: true)]
[Button("清空打包缓存", ButtonSizes.Large)]
private void ClearBuildCache()
{
ClearCache();
}
[ButtonGroup]
[Button("本地普通包", ButtonSizes.Large, ButtonStyle.FoldoutButton)]
private void BuildNormalPackage()
{
StartBuild(PLayerMode.LOCAL_PLAYMODE);
}
[Button("服务器热更包", ButtonSizes.Large, ButtonStyle.FoldoutButton)]
private void BuildServerPackage()
{
StartBuild(PLayerMode.HOST_PLAYMODE);
}
[Title("打包状态", titleAlignment: TitleAlignments.Centered)] [ReadOnly] [LabelText("当前状态")]
public string buildStatus = "就绪";
[ProgressBar(0, 100)] [ReadOnly] [LabelText("打包进度")]
public int buildProgress;
#endregion
#region
[MenuItem("Evo/Dev/Apk打包工具", false, 4)]
private static void ShowWindow()
{
window = GetWindow<BuildApkWindow>(false, "APK打包工具", true);
window.maxSize = new Vector2(500, 700);
window.minSize = new Vector2(400, 600);
window.Show();
}
protected override void Initialize()
{
base.Initialize();
FindScenesWithLoadDll();
UpdateAvailablePackages();
}
#endregion
#region
/// <summary>
/// 开始打包流程
/// </summary>
/// <param name="isWatermark">是否为水印包</param>
private void StartBuild(PLayerMode playMode)
{
if (_isBuilding)
{
Debug.LogWarning("打包正在进行中,请稍候...");
return;
}
_isBuilding = true;
buildStatus = "准备打包...";
buildProgress = 0;
Repaint();
try
{
// 执行打包
BuildAndroid(playMode);
}
catch (Exception e)
{
buildStatus = $"打包失败: {e.Message}";
Debug.LogError($"打包过程中发生错误: {e}");
ShowNotification(new GUIContent("APK打包失败"), 3f);
}
finally
{
_isBuilding = false;
buildProgress = 100;
Repaint();
}
}
/// <summary>
/// Android打包核心逻辑
/// </summary>
private void BuildAndroid(PLayerMode pLayerMode)
{
buildStatus = "加载配置文件...";
Repaint();
buildStatus = "配置包名...";
Repaint();
ConfigurePackageInfo();
buildStatus = "配置场景列表...";
Repaint();
string[] sceneList = ConfigureScenes();
buildStatus = "配置水印信息...";
Repaint();
if (isWatermark)
{
ConfigureWatermark();
}
// 打包前检查
if (!PreBuildCheck())
{
return;
}
buildStatus = "设置打包模式...";
Repaint();
ChangePlayerSchema.SetPlayerMode(pLayerMode);
buildStatus = "执行打包...";
Repaint();
// 配置构建选项
BuildPlayerOptions buildOptions = ConfigureBuildOptions(sceneList, isWatermark);
// 执行打包
var report = BuildPipeline.BuildPlayer(buildOptions);
_lastBuildReport = report;
// 处理打包结果
HandleBuildResult(report, buildOptions.locationPathName);
}
/// <summary>
/// 配置包名和产品名称
/// </summary>
private void ConfigurePackageInfo()
{
string productName;
string packageName;
if (!IsValidPackageName(selectedPackageName))
{
// 不是有效包名格式,添加前缀 com.xosmo.
productName = selectedPackageName;
packageName = "com.xosmo." + selectedPackageName;
}
else
{
// 分割包名字符串,使用最后一个部分作为产品名称
string[] segments = selectedPackageName.Split('.');
productName = segments.Last();
packageName = selectedPackageName;
}
PlayerSettings.productName = productName;
PlayerSettings.applicationIdentifier = packageName;
// 自动递增版本号
if (autoIncrementVersion)
{
IncrementBuildVersion();
}
}
/// <summary>
/// 配置场景列表
/// </summary>
/// <param name="isWatermark">是否为水印包</param>
/// <param name="hotfixMainResDomain">资源配置</param>
/// <returns>场景路径数组</returns>
private string[] ConfigureScenes()
{
List<string> scenes = new List<string>();
string mainScenePath = $"Assets/Main/main_{deviceType.ToString()}.unity";
// 验证主场景是否存在
if (!File.Exists(mainScenePath))
{
throw new Exception($"主场景文件不存在: {mainScenePath}");
}
if (deviceType == DeviceType.Xreal && isWatermark)
{
// 加载资源配置
HotfixMainResDomain hotfixMainResDomain = Resources.Load<HotfixMainResDomain>("HotfixMainResDomain");
if (hotfixMainResDomain == null)
{
throw new Exception("HotfixMainResDomain资源在Resources下不存在请检查");
}
// 水印包包含水印场景
string loadingScenePath = hotfixMainResDomain.projectInfo.loadingScenePath;
if (!string.IsNullOrEmpty(loadingScenePath) && File.Exists(loadingScenePath))
{
scenes.Add(loadingScenePath);
}
else
{
Debug.LogWarning($"加载场景不存在或未配置,仅打包主场景: {loadingScenePath}");
}
}
foreach (var scenePath in _scenePaths)
{
string path = scenePath.Replace("\\", "/");
if (mainScenePath == path)
{
LoadSceneForEditing(mainScenePath);
scenes.Add(mainScenePath);
}
}
return scenes.ToArray();
}
/// <summary>
/// 配置水印信息
/// </summary>
private void ConfigureWatermark()
{
PlayerSettings.productName += "_watermark";
PlayerSettings.applicationIdentifier += "_watermark";
Sprite logoSprite = Resources.Load<Sprite>("logo");
if (logoSprite != null)
{
PlayerSettings.virtualRealitySplashScreen = logoSprite.texture;
PlayerSettings.SplashScreen.showUnityLogo = false;
PlayerSettings.SplashScreen.logos = new[]
{
PlayerSettings.SplashScreenLogo.Create(3f, logoSprite)
};
}
else
{
Debug.LogWarning("Resources下未找到logo图片水印包将不显示自定义logo");
}
}
/// <summary>
/// 配置构建选项
/// </summary>
/// <param name="scenes">场景列表</param>
/// <param name="isWatermark">是否为水印包</param>
/// <returns>构建选项</returns>
private BuildPlayerOptions ConfigureBuildOptions(string[] scenes, bool isWatermark)
{
string buildPath = GetBuildPath(isWatermark);
// 确保构建目录存在
string buildDir = Path.GetDirectoryName(buildPath);
if (!Directory.Exists(buildDir))
{
Directory.CreateDirectory(buildDir);
}
BuildOptions buildOptions = BuildOptions.None;
return new BuildPlayerOptions
{
scenes = scenes,
locationPathName = buildPath,
target = BuildTarget.Android,
options = buildOptions
};
}
/// <summary>
/// 获取构建路径
/// </summary>
/// <param name="isWatermark">是否为水印包</param>
/// <returns>构建路径</returns>
private string GetBuildPath(bool isWatermark)
{
string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
string buildFileName = $"{PlayerSettings.productName}_{timestamp}.apk";
return Path.Combine("Builds", "Android", buildFileName);
}
/// <summary>
/// 处理打包结果
/// </summary>
/// <param name="report">构建报告</param>
/// <param name="buildPath">构建路径</param>
private void HandleBuildResult(UnityEditor.Build.Reporting.BuildReport report, string buildPath)
{
BuildSummary summary = report.summary;
if (summary.result == BuildResult.Succeeded)
{
buildStatus = $"打包成功!大小: {FormatBytes(summary.totalSize)}";
Debug.Log($"构建成功: {buildPath},大小: {FormatBytes(summary.totalSize)}");
ShowNotification(new GUIContent("APK打包成功"), 3f);
// 构建完成后打开文件夹
if (openFolderOnComplete)
{
OpenBuildFolder(Path.GetDirectoryName(buildPath));
}
}
else
{
buildStatus = "打包失败!";
Debug.LogError("构建失败");
ShowNotification(new GUIContent("APK打包失败"), 3f);
// 显示详细错误信息
foreach (var step in report.steps)
{
foreach (var message in step.messages)
{
if (message.type == LogType.Error)
{
Debug.LogError($"构建错误: {message.content}");
}
}
}
}
}
#endregion
#region
/// <summary>
/// 打包前检查
/// </summary>
/// <returns>检查是否通过</returns>
private bool PreBuildCheck()
{
// 检查是否选择了包裹
if (string.IsNullOrEmpty(selectedPackageName))
{
ShowNotification(new GUIContent("请先选择一个包裹!"), 2f);
return false;
}
// 检查当前平台
if (EditorUserBuildSettings.activeBuildTarget != BuildTarget.Android)
{
if (EditorUtility.DisplayDialog("平台提示", "当前平台不是Android是否切换到Android平台", "是", "否"))
{
EditorUserBuildSettings.SwitchActiveBuildTarget(BuildTargetGroup.Android, BuildTarget.Android);
return false; // 切换平台后需要重新检查
}
return false;
}
// 检查场景是否保存
if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo())
{
return false;
}
return true;
}
/// <summary>
/// 清空打包缓存
/// </summary>
private void ClearCache()
{
try
{
buildStatus = "正在清理缓存...";
Repaint();
string projectPath = Application.dataPath;
string libraryPath = Path.GetFullPath(Path.Combine(projectPath, "../Library"));
// 要清理的缓存目录
string[] cacheDirectories =
{
Path.Combine(libraryPath, "BuildCache"),
Path.Combine(libraryPath, "ScriptAssemblies"),
Path.Combine(libraryPath, "ArtifactDB")
};
int clearedCount = 0;
foreach (string cacheDir in cacheDirectories)
{
if (Directory.Exists(cacheDir))
{
Directory.Delete(cacheDir, true);
clearedCount++;
Debug.Log($"成功删除缓存目录: {cacheDir}");
}
}
// 清理构建输出目录
string buildOutputPath = Path.Combine(Application.dataPath, "../Builds/Android");
if (Directory.Exists(buildOutputPath))
{
// 只清理旧的APK文件保留目录结构
string[] oldApks = Directory.GetFiles(buildOutputPath, "*.apk", SearchOption.AllDirectories);
foreach (string apkFile in oldApks)
{
File.Delete(apkFile);
}
Debug.Log("成功清理旧的APK文件");
}
AssetDatabase.Refresh();
buildStatus = "缓存清理完成!";
ShowNotification(new GUIContent("缓存清理完成!"), 2f);
Debug.Log($"缓存清理完成,共清理 {clearedCount} 个缓存目录");
}
catch (Exception e)
{
buildStatus = "缓存清理失败!";
Debug.LogError($"缓存清理失败: {e.Message}");
ShowNotification(new GUIContent("缓存清理失败!"), 2f);
}
}
/// <summary>
/// 获取可用的包裹名称列表
/// </summary>
/// <returns>包裹名称列表</returns>
private List<string> GetAvailablePackageNames()
{
UpdateAvailablePackages();
return CreatAssetWindow.GetCreatDomainAllName().ToList();
}
/// <summary>
/// 更新可用包裹列表
/// </summary>
private void UpdateAvailablePackages()
{
var availablePackages = CreatAssetWindow.GetCreatDomainAllName().ToList();
if (availablePackages.Any() && string.IsNullOrEmpty(selectedPackageName))
{
selectedPackageName = availablePackages.First();
}
}
/// <summary>
/// 当包裹名称改变时的回调
/// </summary>
private void OnPackageNameChanged()
{
buildStatus = "就绪";
}
/// <summary>
/// 自动递增版本号
/// </summary>
private void IncrementBuildVersion()
{
Version version = new Version(PlayerSettings.bundleVersion);
Version newVersion = new Version(version.Major, version.Minor, version.Build + 1);
PlayerSettings.bundleVersion = newVersion.ToString();
Debug.Log($"版本号已递增: {newVersion}");
}
/// <summary>
/// 打开构建文件夹
/// </summary>
/// <param name="folderPath">文件夹路径</param>
private void OpenBuildFolder(string folderPath)
{
if (Directory.Exists(folderPath))
{
EditorUtility.RevealInFinder(folderPath);
}
}
/// <summary>
/// 检测字符串是否是有效的包名格式
/// </summary>
/// <param name="packageName">要检测的字符串</param>
/// <returns>是否是有效的包名格式</returns>
private bool IsValidPackageName(string packageName)
{
if (string.IsNullOrEmpty(packageName))
return false;
// 包名只能包含小写字母、数字和点
foreach (char c in packageName)
{
if (!char.IsLower(c) && !char.IsDigit(c) && c != '.')
return false;
}
// 不能以点开头或结尾
if (packageName.StartsWith(".") || packageName.EndsWith("."))
return false;
// 不能包含连续的点
if (packageName.Contains(".."))
return false;
// 至少包含一个点
if (!packageName.Contains("."))
return false;
return true;
}
/// <summary>
/// 格式化字节大小
/// </summary>
/// <param name="bytes">字节数</param>
/// <returns>格式化后的字符串</returns>
private string FormatBytes(ulong bytes)
{
string[] suffixes = { "B", "KB", "MB", "GB" };
int index = 0;
double size = bytes;
while (size >= 1024 && index < suffixes.Length - 1)
{
size /= 1024;
index++;
}
return $"{size:F2} {suffixes[index]}";
}
#endregion
#region
/// <summary>
/// 查找所有包含LoadDll的场景文件
/// </summary>
private void FindScenesWithLoadDll()
{
List<string> sceneList = new List<string>();
// 搜索所有unity场景文件
string[] allScenes = Directory.GetFiles(Application.dataPath, "*.unity", SearchOption.AllDirectories);
foreach (string scenePath in allScenes)
{
// 读取场景文本内容检查是否包含LoadDll
try
{
string sceneContent = File.ReadAllText(scenePath);
if (sceneContent.Contains("m_Name: LoadDll"))
{
// 转换为Assets相对路径
string assetsPath = scenePath.Replace(Application.dataPath, "Assets");
sceneList.Add(assetsPath);
}
}
catch (Exception e)
{
Debug.LogError($"读取场景文件失败: {scenePath}, 错误: {e.Message}");
}
}
_scenePaths = sceneList.ToArray();
}
/// <summary>
/// 加载场景进行编辑
/// </summary>
private void LoadSceneForEditing(string scenePath)
{
try
{
// 加载场景
EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single);
GameObject loadDllObj = GameObject.Find("LoadDll");
// 收集HybridClREntrance组件
HybridClREntrance entrance = loadDllObj.GetComponent<HybridClREntrance>();
entrance.loadDomain = selectedPackageName;
Debug.Log($"成功加载场景: {scenePath}");
}
catch (Exception e)
{
Debug.LogError($"加载场景失败: {scenePath}, 错误: {e.Message}");
}
}
#endregion
#region
/// <summary>
/// 设备类型
/// </summary>
public enum DeviceType
{
Xreal,
Rokid
}
#endregion
}
}