U3D 自动更新/打Bundle/打包

        这几天搞了个自动打包的工具,开始的时候也是胡搞乱搞一头雾水,一顿查资料啊、翻看别人写的博客啊,终于搞出来了。但是现在回头看,感觉这东西其实就一层窗户纸,捅破之后,也挺简单的~

        公司的项目太大,打一次包真的是慢的一匹... 那么这个打包工具就应运而生了,每天定时执行:先自动更新svn,再打Bundle,再打包,最后邮件通知一下打包结果~

        整体的思路就是通过命令行去执行 更新、打Bundle、打包,然后考虑到mac和windows的电脑使用的命令不一样,那么就用python来做个整合,让它在mac和windows上都可以运行~

        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更新(git同理)

        主要的命令就是 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”。

 

二、自动打Bundle

        这一块,主要思路是用命令行来调用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给我一个准确的已读的尾巴位置,问题迎刃而解~

 

五、参考 及 API

1.《基于python脚本,实现Unity全平台的自动打包》: https://www.cnblogs.com/zblade/p/9298905.html  

2. Unity 官方命令行的 API :https://docs.unity3d.com/560/Documentation/Manual/CommandLineArguments.html 

        

你可能感兴趣的:(U3D 自动更新/打Bundle/打包)