Unity + Jenkins自动打包 (二)构建Jenkins项目以及编写Python、Unity脚本

1、新建Jenkens项目

在上一篇中,完成了Jenkins的安装和初始化,以及权限设置。
查看上一篇:Jenkins安装 点此
现在打开浏览器,输入http://localhost:8081,当然,需要改成你自己设置的Jenkins端口号,然后就会出现一下界面
点击“新建Item”
Unity + Jenkins自动打包 (二)构建Jenkins项目以及编写Python、Unity脚本_第1张图片

输入一个名称,比如现在要构建一个自动打包项目,就叫Build_Apk,然后点击“Freestyle project”
构建一个自由风格的项目。Unity + Jenkins自动打包 (二)构建Jenkins项目以及编写Python、Unity脚本_第2张图片

点击确定,就会生成一个空项目。
Unity + Jenkins自动打包 (二)构建Jenkins项目以及编写Python、Unity脚本_第3张图片

2、添加构建参数

在新建的项目界面中
Unity + Jenkins自动打包 (二)构建Jenkins项目以及编写Python、Unity脚本_第4张图片
勾选第一个“Disard old builds”,它会帮你删除旧的构建记录。
而第二个“This project is parameterized”,意思是这是一个参数化构建的项目,勾选后就可以添加各种参数,如下图:
Unity + Jenkins自动打包 (二)构建Jenkins项目以及编写Python、Unity脚本_第5张图片
比如,我们在Unity打包时,项目上线时会构建一个整包Apk,在上线后,会经常构建热更包,那么就需要添加一个选择参数来区分,如下图:
Unity + Jenkins自动打包 (二)构建Jenkins项目以及编写Python、Unity脚本_第6张图片
然后点击“保存”
Unity + Jenkins自动打包 (二)构建Jenkins项目以及编写Python、Unity脚本_第7张图片
再点击"Build with Parameters",就可以看到刚才添加的参数
Unity + Jenkins自动打包 (二)构建Jenkins项目以及编写Python、Unity脚本_第8张图片
Unity + Jenkins自动打包 (二)构建Jenkins项目以及编写Python、Unity脚本_第9张图片

再点击“配置”,来添加一个Bool 型参数
Unity + Jenkins自动打包 (二)构建Jenkins项目以及编写Python、Unity脚本_第10张图片
Unity + Jenkins自动打包 (二)构建Jenkins项目以及编写Python、Unity脚本_第11张图片
其中“Set by Default”,勾选后这个参数会默认勾选上。
那么这个参数就可以用来选择是否构建AssetBundle,比如第一次出包的时候,肯定就要勾选上,而当我们出包后,测试发现一些配置上的Bug,那么这个时候只需要修改配置表,重新构建Apk即可,无需重新构建AssetBundle,那这时候就不用勾选这个参数。

当然,我们打包时一般来说都会有很多步骤,一般都是一些老前辈写好的静态工具方法,打包的同志按照步骤挨个点一遍,那当然也会出现像上面说到的情况,有时候只是改动一小部分东西,不用全部执行,那就可以添加各种参数来跳过不必要的操作。

3、添加命令、脚本

一般来说,用于打包的Unity工程都在一个专用来打包的主机上,而且一般有两个工程,在其中一个工程里构建AB,再拷贝到另一个不包含各种资源的工程里,然后构建Apk。

那么首先便要将用于构建AB的工程更新,找到“构建”,增加构建步骤,选择“Execute windows batch command”,增加命令行命令。
Unity + Jenkins自动打包 (二)构建Jenkins项目以及编写Python、Unity脚本_第12张图片
以SVN为例,如果是Git,查一下命令就行了
首先找到需要更新的项目目录:
Unity + Jenkins自动打包 (二)构建Jenkins项目以及编写Python、Unity脚本_第13张图片
然后添加命令:
Unity + Jenkins自动打包 (二)构建Jenkins项目以及编写Python、Unity脚本_第14张图片
后面的参数 --accept theirs-conflict,这是用来解决冲突的,以服务器的版本为准。

更新完成后就可以启动Unity工程,等待Unity编译完成后,执行打包的各个步骤。
那么接下来就要编写Unity的静态方法供外部调用:

public class GameBuild {

    public static void StartBuild() {
        // 开始执行各个打包步骤
        // 。。。。。。。。。。。。
        // 。。。。。。。。。。。。
    }
}

那怎么判断Unity已经编译完成了,之后再调用我们写的打包方法呢?

public class GameBuild {

    public static void WaitCompilingToBuild() {
        EditorApplication.update += CanStartBuild;
    }

    static void CanStartBuild() {
        if ( !EditorApplication.isCompiling ) {
            EditorApplication.update -= CanStartBuild;
            Debug.Log( "=====编译完成=====" );
            EditorApplication.delayCall += StartBuild;
        }
    }

    static void StartBuild() {
        // 开始执行各个打包步骤
        // 。。。。。。。。。。。。
        // 。。。。。。。。。。。。
    }
}

现在我们只需要调用 GameBuild.WaitCompilingToBuild,就可以实现在Unity编译完成后调用我们的打包方法。
EditorApplication.delayCall += StartBuild
这一句的意思是:将其执行延迟到检视面板更新完成之后。每个函数在添加后仅执行一次。

现在,在Jenkins里面添加命令
start C:\Progra~1\Unity\Editor\Unity.exe -disable-assembly-updater -projectPath D:\Test_Project -executeMethod GameBuild.WaitCompilingToBuild --Build_AB:%Build_AB%
最后面的参数 --Build_AB:%Build_AB% ,就是用来将我们上面添加的Bool型参数传入Unity,那么Unity如何接收,请往下看
在这里插入图片描述

其中 taskkill /F /IM Unity.exe 是用来关闭Unity进程,因为Unity只能同时存在一个实例

其中start 后面跟Unity.exe的路径;
需要注意 :命令行在执行非CD命令时,无法识别空格,
而Unity一般安装在C:\Program Files下,需要将路径写为:C:\Progra~1

或者先执行 cd 命令进入Unity安装目录,再调用,如图:
Unity + Jenkins自动打包 (二)构建Jenkins项目以及编写Python、Unity脚本_第15张图片

假设打包步骤如我上面所说,在构建AB的工程执行完成后,再将各种资源都拷到构建Apk的工程,那么怎么判断调用的静态方法是否完成。因为调用Unity的方法,命令行不会一直持有Unity,在调用Unity方法后,不会等Unity的方法执行完后再执行下一句命令,所以需要用另外的脚本来判断。

我一般喜欢用Python脚本来判断,关于Python的安装请自行百度。
首先要在打包方法里面增加日志打印,完整代码为:

using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;

public class GameBuild {

    private static string logFilePath = "E:\\Log_Build.txt";
    private static bool Build_AB;

    public static void WaitCompilingToBuild() {
        // 接收外部传入的参数
        string[] args = System.Environment.GetCommandLineArgs();
        foreach ( var s in args ) {
            if ( s.Contains( "--Build_AB:" ) ) {
                Build_AB = s.Split( ':' )[ 1 ] == "true";
            }
        }
        EditorApplication.update += CanStartBuild;
    }

    static void CanStartBuild() {
        if ( !EditorApplication.isCompiling ) {
            EditorApplication.update -= CanStartBuild;
            Debug.Log( "=====编译完成=====" );
            EditorApplication.delayCall += StartBuild;
        }
    }

    static void StartBuild() {
        // 开始执行各个打包步骤

        if ( Build_AB ) {
            // 开始构建AssetBundle
        }
        else {
            // 跳过构建AssetBundle
        }

        WriteLog("Build AB finished");
    }

    public static void WriteLog( string Logstring ) {

        if ( !File.Exists( logFilePath ) ) {
            FileStream stream = File.Create( logFilePath );
            stream.Close();
            stream.Dispose();
        }
        using ( StreamWriter writer = new StreamWriter( logFilePath, true ) ) {
            writer.WriteLine( Logstring );
            writer.Close();
        }
    }
}

那么现在,就只需要监听 Log_Build.txt,这个文件里面是否有日志“Build AB finished”,如果检测到,那么就跳出循环,命令行就可以执行下一步命令。
你可能会问,我为什么不直接使用unity的Debug.log,因为在打包的时候会输出大量的日志,直接检测Unity的日志会消耗比较大。
接下加上Python脚本:

# -*- coding:gb18030 -*-
import os
import time
import sys


# 实时监测unity的log, 参数target_log是我们要监测的目标log, 如果检测到了, 则跳出while循环
def Listen_Log(target_log):
    pos = 0
    while True:
        if os.path.exists(log_file):
            print("监测到日志文件,开始监测打包步骤")
            break
        else:
            print("未监测到日志文件,等待")
            time.sleep(20)
    while True:
        fd = open(log_file, 'r')
        if pos != 0:
            fd.seek(pos, 0)
        while True:
            line = fd.readline()
            pos = pos + len(line)
            if target_log in line:
                print('监测到unity输出了目标log: ' + target_log + '  继续执行下一步==========')
                fd.close()
                return
            if line.strip():
                print(line)
            else:
                break
        fd.close()


if __name__ == '__main__':
    log_file = 'E:/Log_Build.txt'
    print("开始监测日志")
    Listen_Log("Build AB finished")

将脚本命名为Listen_Log.py,然后在Jenkins里面添加命令
Unity + Jenkins自动打包 (二)构建Jenkins项目以及编写Python、Unity脚本_第16张图片

加下来将资源拷到构建Apk的工程里面,直接使用cmd命令:copy pathA pathB
将A文件,拷到B目录下

Unity + Jenkins自动打包 (二)构建Jenkins项目以及编写Python、Unity脚本_第17张图片

接下来在构建Apk的工程里面添加静态方法,同样也需要在Unity编译完成后开始构建Apk
那么,构建Apk就不必使用日志检测的方式判断是否完成,只需要检测目标Apk是否存在即可
那么就需要一个确定的Apk名字,而且外部需要能拿到,那么接下来就用额外参数的形式来调用unity的静态方法
也就是说,Apk的名字由Python脚本确定,然后传给Unity脚本
然后在Apk构建完成后,将Apk复制到大家都能访问到的共享文件夹里面,然后在QQ或者钉钉群里面通知所有人
如果使用QQ的话,打包的主机上需要登录QQ,并且将需要发送消息的QQ群窗口打开
Python脚本,Buid_Apk.py如下

# -*- coding:gb18030 -*-
import os
import time
import win32gui
import win32con
import win32clipboard as w


def call_unity_static_func(func):
    cmd = 'start %s -disable-assembly-updater -projectPath %s -executeMethod %s --pathName:%s'
     % (unity_exe, project_path, func, timeStr)
    print('run cmd:  ' + cmd)
    os.system(cmd)
    print("开始调用打包方法,构建Apk")


# 实时监测Apk是否存在
def Check_Unity_Apk():
    while True:
        if os.path.exists(Apk_file):
            print("监测到Apk,构建Apk成功,开始复制到共享文件夹")
            break
        else:
            print("未监测到Apk,等待一段时间")
            time.sleep(20)


def CopyFile():
    if os.path.exists(targetPath):
        print(" ")
    else:
        print("路径不存在,创建路径: " + targetPath)
        os.mkdir(targetPath)
    while True:
        if os.path.exists(targetPath):
            print("路径存在,开始复制")
            break
        else:
            time.sleep(2)
    cmd = "copy {0} {1}".format(Apk_file, targetPath)
    os.system(cmd)
    time.sleep(5)
    while True:
        if os.path.exists(targetApkPath):
            print("复制到共享文件夹完成,开始通知QQ群")
            break
        else:
            time.sleep(5)


def SendQQ():
    theTime = time.strftime('%Y-%m-%d %H:%M', time.localtime())
    # 发送的消息
    msg = theTime + "\n打包机器人: 构建Apk完成\n最新包位置:" + showApkPath
    # 窗口名字
    name = "XXX项目组"
    # 将测试消息复制到剪切板中
    w.OpenClipboard()
    w.EmptyClipboard()
    w.SetClipboardData(win32con.CF_UNICODETEXT, msg)
    w.CloseClipboard()
    # 获取窗口句柄
    handle = win32gui.FindWindow(None, name)
    # 填充消息
    win32gui.SendMessage(handle, 770, 0, 0)
    # 回车发送消息
    win32gui.SendMessage(handle, win32con.WM_KEYDOWN, win32con.VK_RETURN, 0)


if __name__ == '__main__':
    unity_exe = r'C:\Progra~1\Unity\Editor\Unity.exe'
    project_path = 'D:\Build_Apk_Project'

    # 执行unity静态方法
    timeStr = time.strftime('%Y_%m_%d_%H_%M', time.localtime())
    Apk_file = "E:\\Apk\\" + timeStr + ".apk"

    targetTimeStr = time.strftime('%Y_%m_%d', time.localtime())
    targetPath = "共享文件:\\APK\\" + targetTimeStr
    targetApkPath = targetPath + "\\" + timeStr + ".apk"
    showApkPath = "共享文件/APK/" + targetTimeStr + "/" + timeStr + ".apk"
    static_func = 'GameBuild.WaitCompilingToStartBuildApk'
    call_unity_static_func(static_func)
    print("开始监测日志")
    Check_Unity_Apk()
    time.sleep(2)
    CopyFile()
    time.sleep(2)
    SendQQ()
    time.sleep(2)
    print("构建 Apk 结束")

Unity脚本如下

using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

public class BuildApk
{
    private static string PathName = "";

    public static void WaitCompilingToStartBuildApk() {
    	// 接收外部传入的参数
        string[] args = System.Environment.GetCommandLineArgs();
        foreach ( var s in args ) {
            if ( s.Contains( "--pathName:" ) ) {
                PathName = s.Split( ':' )[ 1 ];
                Debug.LogError( "收到传进来的Apk名: " + PathName );
            }
        }
        EditorApplication.update += CanBuildApk;
    }

    public static void CanBuildApk() {
        if ( !EditorApplication.isCompiling ) {
            EditorApplication.update -= CanBuildApk;
            EditorApplication.delayCall += StartBuildApk;
        }
    }

    public static void StartBuildApk() {
        //版本号
        PlayerSettings.bundleVersion = "1.0.0";
        //API 兼容性等级
        PlayerSettings.SetApiCompatibilityLevel( BuildTargetGroup.Android, ApiCompatibilityLevel.NET_2_0_Subset );
        //最低版本
        PlayerSettings.Android.minSdkVersion = AndroidSdkVersions.AndroidApiLevel21;
        //目标版本
        PlayerSettings.Android.targetSdkVersion = AndroidSdkVersions.AndroidApiLevel26;
        //安装位置 自动 auto
        PlayerSettings.Android.preferredInstallLocation = AndroidPreferredInstallLocation.Auto;
        //使用Gradle进行构建
        EditorUserBuildSettings.androidBuildSystem = AndroidBuildSystem.Gradle;
        //设置包名
        PlayerSettings.applicationIdentifier = "com.公司名.游戏名";
        //产品名字
        PlayerSettings.productName = "啦啦啦";

        PlayerSettings.Android.keystoreName = @"D:\XXXXXXXX\XXX.keystore";
        PlayerSettings.Android.keystorePass = "xxxxxx";

        PlayerSettings.Android.keyaliasName = "android.keystore";
        PlayerSettings.Android.keyaliasPass = "xxxxxx";
        //定义符,添加宏
        PlayerSettings.SetScriptingDefineSymbolsForGroup( BuildTargetGroup.Android, "XXXXXXXXXXXXXX" );

        BuildPlayerOptions buildPlayerOptions = new BuildPlayerOptions();
        buildPlayerOptions.scenes = GetBuildScenes();
        buildPlayerOptions.locationPathName = "D:/Apk/" + PathName + ".apk";
        buildPlayerOptions.target = BuildTarget.Android;
        buildPlayerOptions.options = BuildOptions.None;
        //执行打包 场景名字,打包路径
        BuildPipeline.BuildPlayer( buildPlayerOptions );

        Debug.Log( "构建Apk完成" );
    }

    static string[] GetBuildScenes() {
        List<string> names = new List<string>();
        foreach ( EditorBuildSettingsScene e in EditorBuildSettings.scenes ) {
            if ( e == null )
                continue;
            if ( e.enabled )
                names.Add( e.path );
        }
        return names.ToArray();
    }
}

代码逻辑就是:由Python去调用Unity的静态方法,并且在调用时传入一个字符串参数作为Apk的名字,然后在Python脚本里面监测Apk是否存在,然后拷到共享文件夹内,然后通知QQ群。

接下来在Jenkins里面添加命令
Unity + Jenkins自动打包 (二)构建Jenkins项目以及编写Python、Unity脚本_第18张图片

4、完结

那么关于Unity + Jenkins自动打包的东西就讲到这儿,有什么不对的地方欢迎各位大佬在评论区指出!
有什么问题也可以在CSDN上私信问我,博客名:水星程序店!

你可能感兴趣的:(Unity,unity,jenkins,python,自动化)