这几天搞了个自动打包的工具,开始的时候也是胡搞乱搞一头雾水,一顿查资料啊、翻看别人写的博客啊,终于搞出来了。但是现在回头看,感觉这东西其实就一层窗户纸,捅破之后,也挺简单的~
公司的项目太大,打一次包真的是慢的一匹... 那么这个打包工具就应运而生了,每天定时执行:先自动更新svn,再打Bundle,再打包,最后邮件通知一下打包结果~
ps : 因为苹果开发账号过期了... 这篇博客mac上打包就写到打出xcode工程为止 ....
1. python、Unity
2. litJson(打包配置用,这里图方便就用了json,也可以用xml 或者 自己的excel倒数据的工具)
3. 一个py脚本,存储工具运行需要的各种配置(Unity安装位置啊、项目路径啊、 blablabla 具体如下:)
config_autoPackage.py
# 本地Unity.exe位置
unity_exe = 'F:/unity/Editor/Unity.exe'
unity_app = '/Applications/Unity/Unity.app/Contents/MacOS/Unity'
# unity工程根目录
project_path = 'E:/U_Project/MyProject'
project_path_mac = "/Volumes/'MacOS HD Data'/MyProject"
# bundleLog位置
bundle_log = 'C:/Users/hp/Desktop/AutoPackageTest/unity_bundle_log.log'
bundle_log_mac = '/Users/xiaosu/Desktop/AutoPackageTest/unity_bundle_log.log'
# packageLog位置
package_log = 'C:/Users/hp/Desktop/AutoPackageTest/unity_package_log.log'
package_log_mac = '/Users/xiaosu/Desktop/AutoPackageTest/unity_package_log.log'
# 调用打bundle接口
bundle_fuc = 'AutoPackage.BuildBundle'
# 调用打包接口
package_fuc = 'AutoPackage.BuildApk'
# 需要revert的外链1
external_link1 = 'Assets\SharedAssets'
# 需要revert的外链2
external_link2 = 'Assets\VarietyStore'
主要的命令就是 svn update,但是害怕出现各种意料之外的比如说冲突(命令行执行更新SVN的时候,一旦遇到冲突,就会给出选项让你手动键入解决方案,这时候进程就卡在 “选择题” 这儿了 ... 所以一定尽可能避免冲突),所以我选择在update之前先clearup 再 revert (我们公司有打包机专门打包用的,一般不会在打包机上对逻辑和资源进行修改,所以才这么操作),保证万无一失之后再update。
代码如下:
AutoUpdateSVN.py
import os
import config_autoPackage
import platform
__cmdCleanup = ' && svn cleanup && svn revert --recursive .'
__cmdUpdate = ' && svn update'
class P:
if_mac = False
get_dic_cmd = ''
def start_update():
m_platform = platform.system()
print('Current platform : ' + m_platform)
if m_platform == 'Darwin':
P.if_mac = True
else:
P.if_mac = False
__prepare_cleanup_SVN()
def __prepare_cleanup_SVN():
print('开始cleanup')
if P.if_mac:
P.get_dic_cmd = "cd / && cd " + config_autoPackage.project_path_mac
else:
path1 = config_autoPackage.project_path.split('/')[0]
path2 = config_autoPackage.project_path.split(':/')[1]
print("盘符: " + path1)
print("路径: " + path2)
P.get_dic_cmd = path1 + " && cd / && cd " + path2
cmd = P.get_dic_cmd + __cmdCleanup
print(cmd)
t = os.system(cmd)
print("prepare_cleanup_SVN :" + str(t))
__prepare_revert_SVN()
def __prepare_revert_SVN():
print('开始revert')
cmd = '%s && svn revert %s -R && svn revert %s -R' % \
(P.get_dic_cmd, config_autoPackage.external_link1,
config_autoPackage.external_link2)
print(cmd)
t = os.system(cmd)
print("prepare_revert_SVN :" + str(t))
__update_SVN()
def __update_SVN():
print('开始update')
cmd = P.get_dic_cmd + __cmdUpdate
print(cmd)
t = os.system(cmd)
if t == 0:
return True
else:
return False
if __name__ == '__main__':
start_update()
代码可能写的比较乱(python新手,见谅见谅),思路就是通过命令行切换到对应的目录下,然后按顺序执行clearup,revert,update。
需要注意的几点:
1. 多条命令一起调用的时候 用 && 隔开,这样会逐条执行而不会同时执行。
2. 根目录下revert并不会revert外链的目录,所以代码中,外链的目录是要单独revert的。
3. 完美执行命令行之后,系统给一个返回值“0”。
这一块,主要思路是用命令行来调用Unity-Editor的静态方法,不必多说,上代码:
AutoBundle.py
import os
import time
import platform
import subprocess
import config_autoPackage
class P:
if_mac = False
log_path = ''
def start_bundle():
m_platform = platform.system()
if m_platform == 'Darwin':
P.if_mac = True
else:
P.if_mac = False
if not P.if_mac:
__kill_unity()
time.sleep(1)
__clear_log()
time.sleep(1)
__start_build_bundle()
__monitor_unity_log()
print('Build_Bundle_Done')
def __kill_unity():
os.system('taskkill /IM Unity.exe /F')
def __clear_log():
if P.if_mac:
P.log_path = config_autoPackage.bundle_log_mac
else:
P.log_path = config_autoPackage.bundle_log
if os.path.exists(P.log_path):
os.remove(P.log_path)
def __start_build_bundle():
if P.if_mac:
cmd = '%s -projectPath %s -executeMethod %s -logFile %s -batchmode -quit' \
% (config_autoPackage.unity_app, config_autoPackage.project_path_mac,
config_autoPackage.bundle_fuc, config_autoPackage.bundle_log_mac)
else:
cmd = '%s -projectPath %s -executeMethod %s -logFile %s -batchmode -quit' \
% (config_autoPackage.unity_exe, config_autoPackage.project_path,
config_autoPackage.bundle_fuc, config_autoPackage.bundle_log)
print(cmd)
subprocess.Popen(cmd)
def __monitor_unity_log():
print("__monitor_unity_log")
pos = 0
while True:
if os.path.exists(P.log_path):
break
else:
time.sleep(0.1)
while True:
fd = open(P.log_path, 'r', encoding='utf-8')
if 0 != pos:
fd.seek(pos, 0)
while True:
line = fd.readline()
pos = fd.tell()
if 'BatchMode: Unity has not been activated' in line:
print('失败 :Unity 需要重新激活!!! ')
fd.close()
return
if 'Exiting batchmode successfully' in line:
print('Bundle 成功 :Exiting batchmode successfully')
fd.close()
return
if 'Scripts have compiler errors.' in line:
print('代码编译错误!!!')
fd.close()
return
if line.strip():
print(line)
else:
break
fd.close()
if __name__ == "__main__":
start_bundle()
C#部分的代码放到 下面~
三、自动Package
这一块和上面打Bundle一样,py方面就是调用命令行,上代码:
AutoBundle.py
import os
import time
import platform
import subprocess
import config_autoPackage
class P:
if_mac = False
log_path = ''
def start_package():
m_platform = platform.system()
if m_platform == 'Darwin':
P.if_mac = True
else:
P.if_mac = False
if not P.if_mac:
__kill_unity()
time.sleep(1)
__clean_log()
time.sleep(1)
__start_build_package()
__monitor_unity_log()
def __kill_unity():
os.system('taskkill /IM Unity.exe /F')
def __clean_log():
if P.if_mac:
P.log_path = config_autoPackage.bundle_log_mac
else:
P.log_path = config_autoPackage.bundle_log
if os.path.exists(P.log_path):
os.remove(P.log_path)
def __start_build_package():
if P.if_mac:
cmd = '%s -projectPath %s -logFile %s -executeMethod %s -batchmode -quit' % \
(config_autoPackage.unity_app, config_autoPackage.project_path_mac,
P.log_path, config_autoPackage.package_fuc)
else:
cmd = '%s -projectPath %s -logFile %s -executeMethod %s -batchmode -quit' % \
(config_autoPackage.unity_exe, config_autoPackage.project_path,
P.log_path, config_autoPackage.package_fuc)
print("run : " + cmd)
subprocess.Popen(cmd)
def __monitor_unity_log():
pos = 0
while True:
if os.path.exists(P.log_path):
break
else:
time.sleep(0.1)
while True:
fd = open(P.log_path, 'r', encoding='UTF-8')
if 0 != pos:
fd.seek(pos, 0)
while True:
line = fd.readline()
pos = fd.tell()
if line.strip():
print(line)
if 'is an incorrect path for a scene file: Build Failed' in line:
print('打包失败 :错误的场景路径 看Log!!!')
fd.close()
return
if 'Scripts have compiler errors.' in line:
print('代码编译错误!!!')
fd.close()
return
if 'DisplayProgressNotification: Build Failed' in line:
print('打包失败 看Log!!!')
fd.close()
return
if 'There is no Json at' in line:
print('目标路径 缺少配置Json文件!!!')
fd.close()
return
if 'Exiting batchmode successfully' in line:
print('Package成功 : Exiting batchmode successfully')
fd.close()
return
else:
break
fd.close()
if __name__ == '__main__':
start_package()
C#部分的代码:AutoPackage.cs
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using System;
using System.IO;
using LitJson;
public class AutoPackage : EditorWindow
{
static readonly string jsonPath = "C:/Users/hp/Desktop/AutoPackageTest/BuildPackageConfig.txt";
static BuildPlayerOptions m_buildOption;
public static void BuildApk()
{
Debug.Log("Build Apk ... ");
ReadBuildConfig();
BuildPipeline.BuildPlayer(m_buildOption);
}
public static void BuildBundle()
{
Debug.Log("Build Bundle ... ");
Framework.Resource.CustomBundleBuilder.CallBuildByCMD();
}
private static void ReadBuildConfig()
{
Debug.Log("Start Read Build Config ... ");
m_buildOption = new BuildPlayerOptions();
JsonDataEx jd;
if (File.Exists(jsonPath))
{
string str = File.ReadAllText(jsonPath);
jd = JsonMapper.ToObject(str);
}
else
{
Debug.LogError("There is no Json at : " + jsonPath);
return;
#region ForTest
//jd = new JsonDataEx();
//jd["productName"] = "test";
//jd["companyName"] = "justTEST";
//jd["packagePath"] = "C:/Users/hp/Desktop/AutoPackageTest/";
//jd["packageName"] = "test.apk";
//jd["version"] = "1.0.0";
//jd["scenePath"] = "Assets/";
//jd["defineInAndriod"] = "fuck1;fuck2";
//jd["defineInIOS"] = "fuck3;fuck4";
//jd["targetPlatform"] = "A";
//jd["senceList"] = new JsonDataEx
//{
// "1.unity",
// "2.unity"
//};
//File.WriteAllText("C:/Users/hp/Desktop/AutoPackageTest/jsonConfig.txt", jd.ToJson());
#endregion
};
//名字
Debug.Log("包名 :" + jd["productName"]);
PlayerSettings.productName = jd["productName"].ToString();
Debug.Log("公司名 :" + jd["companyName"]);
PlayerSettings.companyName = jd["companyName"].ToString();
//打包版本号
Debug.Log("打包版本 :" + jd["version"]);
PlayerSettings.bundleVersion = jd["version"].ToString();
//添加宏定义
Debug.Log("安卓宏定义 : " + jd["defineInAndriod"]);
PlayerSettings.SetScriptingDefineSymbolsForGroup(BuildTargetGroup.Android, jd["defineInAndriod"].ToString());
Debug.Log("苹果宏定义 : " + jd["defineInIOS"]);
PlayerSettings.SetScriptingDefineSymbolsForGroup(BuildTargetGroup.iOS, jd["defineInIOS"].ToString());
//打包需要的各个场景
Debug.Log("场景路径 : " + jd["scenePath"]);
string scenePath = jd["scenePath"].ToString();
List tmp = new List();
for (int i = 0; i < jd["senceList"].Count; i++)
{
Debug.Log("需打入场景 :" + jd["senceList"][i].ToString());
tmp.Add(scenePath + jd["senceList"][i].ToString());
}
m_buildOption.scenes = tmp.ToArray();
//打包平台
Debug.Log("目标平台 : " + jd["targetPlatform"]);
m_buildOption.target = jd["targetPlatform"].ToString() == "A" ? BuildTarget.Android : BuildTarget.iOS;
//打包路径 + 包名
Debug.Log("包路径 : " + jd["packagePath"] + jd["packageName"]);
m_buildOption.locationPathName = jd["packagePath"].ToString() + jd["packageName"].ToString();
//屏幕方向
PlayerSettings.defaultInterfaceOrientation = UIOrientation.LandscapeLeft;
//打包环境
ApiCompatibilityLevel api = ApiCompatibilityLevel.NET_2_0;
string apis = jd["APILevel"].ToString();
if (!String.IsNullOrEmpty(apis))
{
if (apis.Contains("2_0_S"))
{
api = ApiCompatibilityLevel.NET_2_0_Subset;
}
else if (apis.Contains("2_0"))
{
api = ApiCompatibilityLevel.NET_2_0;
}
else if (apis.Contains("4_6"))
{
api = ApiCompatibilityLevel.NET_4_6;
}
else if (apis.Contains("Micro"))
{
api = ApiCompatibilityLevel.NET_Micro;
}
else if (apis.Contains("Web"))
{
api = ApiCompatibilityLevel.NET_Web;
}
}
Debug.Log("API兼容 :" + api.ToString());
PlayerSettings.SetApiCompatibilityLevel
(jd["targetPlatform"].ToString() == "A" ? BuildTargetGroup.Android : BuildTargetGroup.iOS, api);
//Andriod 设置
AndroidTargetDevice dev = AndroidTargetDevice.ARMv7;
string devices = jd["AndroidTarget"].ToString();
if (!String.IsNullOrEmpty(devices))
{
if (devices.Contains("FAT"))
{
dev = AndroidTargetDevice.FAT;
}
else if (devices.Contains("ARMv7"))
{
dev = AndroidTargetDevice.ARMv7;
}
else if (devices.Contains("x86"))
{
dev = AndroidTargetDevice.x86;
}
}
Debug.Log("AndroidTargetDevice : " + dev.ToString());
PlayerSettings.Android.targetDevice = dev;
int bundleVersionCode = 63374379;
int.TryParse(jd["AndroidBundleVersion"].ToString(), out bundleVersionCode);
Debug.Log("AndroidBundleVersion : " + bundleVersionCode);
PlayerSettings.Android.bundleVersionCode = bundleVersionCode;
AndroidSdkVersions sdkV = AndroidSdkVersions.AndroidApiLevel19;
string version = jd["AndroidSdkMinVersion"].ToString();
try
{
sdkV = (AndroidSdkVersions)Enum.Parse(typeof(AndroidSdkVersions), version);
}
catch
{
Debug.Log("读取AndroidSdkVersions失败, 默认:AndroidApiLevel19");
}
Debug.Log("AndroidSdkMinVersion : " + sdkV.ToString());
PlayerSettings.Android.minSdkVersion = sdkV;
Debug.Log("keystoreName : " + jd["keystoreName"]);
PlayerSettings.Android.keystoreName = jd["keystoreName"].ToString();
Debug.Log("keyaliasName : " + jd["keyaliasName"]);
PlayerSettings.Android.keyaliasName = jd["keyaliasName"].ToString();
//密码
Debug.Log("keyaliasPass : " + jd["keyaliasPass"]);
PlayerSettings.keyaliasPass = jd["keyaliasPass"].ToString();
Debug.Log("keystorePass : " + jd["keystorePass"]);
PlayerSettings.keystorePass = jd["keystorePass"].ToString();
Debug.Log("Read Build Config End !");
}
}
///JsonData拓展类
public class JsonDataEx : LitJson.JsonData
{
public new LitJson.JsonData this[string key]
{
get
{
try
{
return (this as JsonData)[key];
}
catch (Exception e)
{
Debug.LogError("The KEY : " + key + " 出错!!! || " + e);
return "";
}
}
set
{
(this as JsonData)[key] = value;
}
}
}
上面这段代码逻辑还是很清晰的,就是通过 Editor下的静态方法来打Bundle,和Package。
其中读打Bundle的配置工具是事先同事写好了的,代码量不小就不贴在这里了。
打包的配置我整合在了json里,差不多这些应该够了,根据自己的需要再增删改一波就可以实用了~
激动的心,颤抖的手,小李第一篇博客有没有!
工作马上3年了,第一次写博客分享代码,记录心得,还是有点小紧脏~~~
这次写自动打包,下次搞一个新手版的socket吧,学习使用~
咳咳,扯远了....
1. 实际工作中,还可以用Jekins或者各种定时任务类的工具来定时触发主脚本,每天一个无人看守版测试包,在每个流程失败的节点发送邮件告知 工作人员哪里出了问题,或者打包成功。
2. 调用Unity命令行的时候, -batchmode 挺好用,可以在不打开Unity窗口界面的情况下,在后台去调用你让它执行的方法,很大的提升了运行效率,但是如果有大量资源需要加载的话,放心还是挺慢的~ 可以通过log去看,十几万行的log会告诉你,Unity都干了啥~ 红红火火恍恍惚惚哈 ~
3. Unity给出的log的编码格式是 utf-8,可能会出现python解析不了的情况,解决办法就是:
fd = open( xxxxxx ,'r', encoding='utf-8')
4. 之前运行的时候遇到过一个让人很头疼的问题,一直报错提示编码格式解析不出来,而且每次都是在解析到中文行之后就报错,究其原因,是因为在读log的时候,用pos来定位读到了哪,pos += len(line),然后在下次循环的时候 fd.seek(pos, 0) 来接着读。但是!!! len(line)在遇到了中文的时候 返回的长度是不准确的!!!这样导致 下次循环再读的时候,起始位置没在一个完整的字开头的位置,所以就解析出错了。最后将pos的赋值 改成了 fd.tell() 让python给我一个准确的已读的尾巴位置,问题迎刃而解~
1.《基于python脚本,实现Unity全平台的自动打包》: https://www.cnblogs.com/zblade/p/9298905.html
2. Unity 官方命令行的 API :https://docs.unity3d.com/560/Documentation/Manual/CommandLineArguments.html