1、新建Jenkens项目
在上一篇中,完成了Jenkins的安装和初始化,以及权限设置。
查看上一篇:Jenkins安装 点此
现在打开浏览器,输入http://localhost:8081,当然,需要改成你自己设置的Jenkins端口号,然后就会出现一下界面
点击“新建Item”
输入一个名称,比如现在要构建一个自动打包项目,就叫Build_Apk,然后点击“Freestyle project”
构建一个自由风格的项目。
2、添加构建参数
在新建的项目界面中
勾选第一个“Disard old builds”,它会帮你删除旧的构建记录。
而第二个“This project is parameterized”,意思是这是一个参数化构建的项目,勾选后就可以添加各种参数,如下图:
比如,我们在Unity打包时,项目上线时会构建一个整包Apk,在上线后,会经常构建热更包,那么就需要添加一个选择参数来区分,如下图:
然后点击“保存”
再点击"Build with Parameters",就可以看到刚才添加的参数
再点击“配置”,来添加一个Bool 型参数
其中“Set by Default”,勾选后这个参数会默认勾选上。
那么这个参数就可以用来选择是否构建AssetBundle,比如第一次出包的时候,肯定就要勾选上,而当我们出包后,测试发现一些配置上的Bug,那么这个时候只需要修改配置表,重新构建Apk即可,无需重新构建AssetBundle,那这时候就不用勾选这个参数。
当然,我们打包时一般来说都会有很多步骤,一般都是一些老前辈写好的静态工具方法,打包的同志按照步骤挨个点一遍,那当然也会出现像上面说到的情况,有时候只是改动一小部分东西,不用全部执行,那就可以添加各种参数来跳过不必要的操作。
3、添加命令、脚本
一般来说,用于打包的Unity工程都在一个专用来打包的主机上,而且一般有两个工程,在其中一个工程里构建AB,再拷贝到另一个不包含各种资源的工程里,然后构建Apk。
那么首先便要将用于构建AB的工程更新,找到“构建”,增加构建步骤,选择“Execute windows batch command”,增加命令行命令。
以SVN为例,如果是Git,查一下命令就行了
首先找到需要更新的项目目录:
然后添加命令:
后面的参数 --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安装目录,再调用,如图:
假设打包步骤如我上面所说,在构建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里面添加命令
加下来将资源拷到构建Apk的工程里面,直接使用cmd命令:copy pathA pathB
将A文件,拷到B目录下
接下来在构建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群。
4、完结
那么关于Unity + Jenkins自动打包的东西就讲到这儿,有什么不对的地方欢迎各位大佬在评论区指出!
有什么问题也可以在CSDN上私信问我,博客名:水星程序店!