0%

Unity3D使用Jenkins自动构建apk

记录使用命令行方式调用Unity打apk包及Jenkins集成的一些方法。

打包相关的API

BuildTarget

枚举类型,指定要构建的目标平台。如选择目标平台为BuildTarget.iOSBuildTarget.Android

BuildTargetGroup

枚举类型,指定要构建的目标组。如选择目标组为BuildTargetGroup.iOSBuildTargetGroup.Android

BuildOptions

枚举类型,构建选项,可以将多个选项组合在一起。以下是支持的一些枚举值,详细参考BuildOptions

描述
None Perform the specified build without any special settings or extra tasks.
Development Build a development version of the player.
AutoRunPlayer Run the built player.
ShowBuiltPlayer Show the built player.
BuildAdditionalStreamedScenes Build a compressed asset bundle that contains streamed scenes loadable with the UnityWebRequest class.
AcceptExternalModificationsToPlayer Used when building Xcode (iOS) or Eclipse (Android) projects.
ConnectWithProfiler Start the player with a connection to the profiler in the editor.
AllowDebugging Allow script debuggers to attach to the player remotely.
SymlinkLibraries Symlink runtime libraries when generating iOS Xcode project. (Faster iteration time).
UncompressedAssetBundle Don’t compress the data when creating the asset bundle.
ConnectToHost Sets the Player to connect to the Editor.
EnableHeadlessMode Build headless Linux standalone.
BuildScriptsOnly Build only the scripts of a project.
ForceEnableAssertions Include assertions in the build. By default, the assertions are only included in development builds.
CompressWithLz4 Use chunk-based LZ4 compression when building the Player.
CompressWithLz4HC Use chunk-based LZ4 high-compression when building the Player.
StrictMode Do not allow the build to succeed if any errors are reporting during it.

BuildPlayerOptions

提供用于控制BuildPipeline.BuildPlayer的行为的多种选项,详细参考BuildPlayerOptions

描述
assetBundleManifestPath The path to an manifest file describing all of the asset bundles used in the build (optional).
locationPathName The path where the application will be built.
options Additional BuildOptions, like whether to run the built player.
scenes The scenes to be included in the build. If empty, the currently open scene will be built. Paths are relative to the project folder (Assets/MyLevels/MyScene.unity).
target The BuildTarget to build.
targetGroup The BuildTargetGroup to build.

BuildPipeline.BuildPlayer

构建打包工作其实是通过调用BuildPipeline.BuildPlayer来完成的:

1
2
3
4
5
public static string BuildPlayer(BuildPlayerOptions buildPlayerOptions);
// or
public static string BuildPlayer(string[] levels, string locationPathName, BuildTarget target, BuildOptions options);
// or
public static string BuildPlayer(EditorBuildSettingsScene[] levels, string locationPathName, BuildTarget target, BuildOptions options);

需要提供参数buildPlayerOptions或对应的levelslocationPathNametargetoptions

封装并提供静态方法

为了完成自动构建,需要对之前的构建方法进行封装,并提供一个外部方法供外部的shell脚本(或bat脚本、或python脚本)调用。同时还需要处理一些其他的工作,如处理由shell脚本向Unity传入的参数,以定制构建的内容(如版本、平台、渠道等)。

命令行参数获取

1
string[] args = System.Environment.GetCommandLineArgs();

此方法得到字符串数组,遍历数组并匹配其中的字符串,以判断构建的版本、平台、渠道等。

修改宏定义

使用以下方法修改PlayerSetting的宏定义,注意会覆盖掉原来已有的宏定义。

1
public static void SetScriptingDefineSymbolsForGroup(BuildTargetGroup targetGroup, string defines);

参数defines是由分号连接的各个宏组成的字符串。

可以用PlayerSettings.GetScriptingDefineSymbolsForGroup(BuildTargetGrouptargetGroup)来获取当前原有的宏。

选择构建的场景

处理构建的场景,将其转化为字符串数组(也可以直接用EditorBuildSettingsScene的数组):

1
2
3
4
5
6
7
8
9
10
11
12
static string[] GetBuildScenes()
{
List<string> names = new List<string>();
foreach(EditorBuildSettingsScene e in EditorBuildSettings.scenes)
{
if (e != null && e.enabled)
{
names.Add (e.path);
}
}
return names.ToArray();
}

构建与构建前后处理

在构建的前后需要的额外的工作,如构建之前,需要处理脚本传入的参数,以更新构建的参数。除此之外还可能需要绑定生成lua文件、生成AssetBundle等。

完整的脚本

以下是完整的C#脚本,ProjectBuild.cs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
using UnityEngine;
using UnityEditor;
using System.Collections;
using System.Collections.Generic;

public class ProjectBuild
{
private static string m_flavor = "";
private static string m_svnRevision = "";
private static BuildTarget m_buildTarget = BuildTarget.Android;
private static string BUILD_OUTPUT_ANDROID = "Bin/Android/";

[MenuItem("Tools/AndroidBuild/Default", false, 100)]
public static void Build()
{
PreBuild ();
BuildPlayer ();
PostBuild ();
}

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

private static void UpdateBuildParam()
{
string commonDefineSymbols = "ASYNC_MODE;USING_DOTWEENING;";
string defineSymbolsForFlavor = "";

string[] args = System.Environment.GetCommandLineArgs();
foreach(string arg in args)
{
if (arg != null && arg.Length > 0)
{
string oneArg = arg.ToLower ();
if (oneArg.StartsWith("flavor"))
{
string[] flavorArr = oneArg.Split ('=');
if(flavorArr.Length > 1)
{
m_flavor = flavorArr[1];
defineSymbolsForFlavor = m_flavor.ToUpper() + ";";
}
}
else if (oneArg.StartsWith("svn"))
{
string[] svnArr = oneArg.Split ('=');
if(svnArr.Length > 1)
{
m_svnRevision = svnArr[1];
}
}
}
}

string defineSymbols = commonDefineSymbols + defineSymbolsForFlavor;

PlayerSettings.SetScriptingDefineSymbolsForGroup (BuildTargetGroup.Android, defineSymbols);
}

private static void PreBuild()
{
Debug.Log("PreBuild Start");

UpdateBuildParam ();

//ToLuaMenu.OnPreBuild ();

Debug.Log("BuildAndroidRes");

//Packager.BuildAndroidRes ();

Debug.Log("PreBuild Finish");
}

private static void BuildPlayer()
{
Debug.Log("BuildPlayer Start");

BuildOptions buildOption = BuildOptions.None;

string locationPathName;

locationPathName = BUILD_OUTPUT_ANDROID;
System.DateTime time = System.DateTime.Now;
string timeStr = time.Month.ToString ("D2") + time.Day.ToString ("D2") + "_" + time.Hour.ToString ("D2") + time.Minute.ToString ("D2");
locationPathName += "GAME_" + timeStr + "_" + m_svnRevision + "_" + m_flavor + ".apk";
BuildPipeline.BuildPlayer(GetBuildScenes(), locationPathName, m_buildTarget, buildOption);

Debug.Log("BuildPlayer Finish");
}

private static void PostBuild()
{
Debug.Log("PostBuild");
}

}

shell脚本调用

命令行运行Unity

shell脚本中首先需要定义一些路径变量,当然这些也可以直接配置在环境变量里,包括Unity程序的位置,项目工程的目录:

1
2
UNITY_PATH=/Applications/Unity/Unity.app/Contents/MacOS/Unity
PROJECT_PATH=/Users/aillieo/PROJECT/game

使用命令行方式运行Unity,执行以下命令开始构建,其它相关参数可以详见CommandLineArguments

1
$UNITY_PATH -projectPath $PROJECT_PATH -executeMethod ProjectBuild.Build -batchmode -quit $@

其它操作与处理

svn相关

在构建之前,一般需要获取最新代码,切换到项目目录并拉取svn最新代码:

1
2
cd $PROJECT_PATH
svn up

如果需要当前svn版本号,可以使用以下的方法提取,保存该变量并传给Unity。

1
2
svn info | grep Revision: | awk '{print $2}' 
#svn_rev=$(svn info | grep Revision: | awk '{print $2}')

记录log

如需记录log,在执行Unity构建方法时提供参数如下,即可保存log到指定文件:

1
-logFile log.txt

移动文件

构建完成之后,将构建完成的apk移动到指定的目录:

1
2
3
4
5
APK_BUILD_PATH=$PROJECT_PATH/Bin/Android

JENKINS_APK_PATH=$WORKSPACE

mv $APK_BUILD_PATH/* $JENKINS_APK_PATH

完整的shell脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 配置路径
UNITY_PATH=/Applications/Unity/Unity.app/Contents/MacOS/Unity
PROJECT_PATH=/Users/aillieo/PROJECT/game

APK_BUILD_PATH=$PROJECT_PATH/Bin/Android
JENKINS_APK_PATH=$WORKSPACE

# 更新代码
cd $PROJECT_PATH
svn up

# 获取svn版本号
svn_rev=$(svn info | grep Revision: | awk '{print $2}')

# 调用构建的静态方法
$UNITY_PATH -projectPath $PROJECT_PATH -executeMethod ProjectBuild.Build -batchmode -quit $@ svn=$svn_rev

# 移动构建完成的apk
mv $APK_BUILD_PATH/* $JENKINS_APK_PATH

Jenkins配置

工作空间

配置JENKINS_HOME,有项目工作空间目录:

alt text

构建的shell脚本内部可以使用$WORKSPACE来获取当前的工作空间目录。

配置构建参数

可以创建多种类型的参数,这里以选项参数为例,其它类型参数同理:

alt text

alt text

调用shell脚本

调用脚本并直接传入构建参数:

alt text

alt text

实时输出log

此步骤的目的是将Unity产生的log实时输出到Jenkins的ConsoleOutput中。Unity通过命令行方式执行方法时不能直接将log(如代码中Debug.Log()方法输出的内容等)输出到控制台,因此在Jenkins中也无法实时查看。网上有一种借助python来实现的方法,可解决这一问题,详见Unity命令行模式,也能「日志实时输出」

REFERENCE

https://docs.unity3d.com/Manual/CommandLineArguments.html

https://docs.unity3d.com/ScriptReference/BuildPipeline.BuildPlayer.html

https://www.jianshu.com/p/a9261113b4ac

http://www.xuanyusong.com/archives/2748

https://www.jianshu.com/p/bd97cb8042a9